View Javadoc

1   /**
2    * Copyright 2005-2012 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.kuali.rice.krad.uif.UifConstants;
20  import org.kuali.rice.krad.uif.UifPropertyPaths;
21  import org.kuali.rice.krad.uif.container.CollectionGroup;
22  import org.kuali.rice.krad.uif.field.DataField;
23  import org.kuali.rice.krad.uif.view.View;
24  import org.kuali.rice.krad.uif.component.BindingInfo;
25  import org.kuali.rice.krad.uif.component.Component;
26  import org.kuali.rice.krad.uif.layout.LayoutManager;
27  
28  import java.util.HashMap;
29  import java.util.List;
30  import java.util.Map;
31  
32  /**
33   * Utility class for UIF expressions
34   *
35   * @author Kuali Rice Team (rice.collab@kuali.org)
36   */
37  public class ExpressionUtils {
38  
39      /**
40       * Adjusts the property expressions for a given object. Any nested properties are moved to the parent
41       * object. Binding adjust prefixes are replaced with the correct values.
42       *
43       * <p>
44       * The org.kuali.rice.krad.uif.UifConstants#NO_BIND_ADJUST_PREFIX prefix will be removed
45       * as this is a placeholder indicating that the property is directly on the form.
46       * The org.kuali.rice.krad.uif.UifConstants#FIELD_PATH_BIND_ADJUST_PREFIX prefix will be replaced by
47       * the object's field path - this is only applicable to DataFields. The
48       * org.kuali.rice.krad.uif.UifConstants#DEFAULT_PATH_BIND_ADJUST_PREFIX prefix will be replaced
49       * by the view's default path if it is set.
50       * </p>
51       *
52       * @param view - the parent view of the object
53       * @param object - Object to adjust property expressions on
54       */
55      public static void adjustPropertyExpressions(View view, Object object) {
56          if (object == null) {
57              return;
58          }
59  
60          // get the map of property expressions to adjust
61          Map<String, String> propertyExpressions = new HashMap<String, String>();
62          if (Component.class.isAssignableFrom(object.getClass())) {
63              propertyExpressions = ((Component) object).getPropertyExpressions();
64          } else if (LayoutManager.class.isAssignableFrom(object.getClass())) {
65              propertyExpressions = ((LayoutManager) object).getPropertyExpressions();
66          } else if (BindingInfo.class.isAssignableFrom(object.getClass())) {
67              propertyExpressions = ((BindingInfo) object).getPropertyExpressions();
68          }
69  
70          Map<String, String> adjustedPropertyExpressions = new HashMap<String, String>();
71          for (Map.Entry<String, String> propertyExpression : propertyExpressions.entrySet()) {
72              String propertyName = propertyExpression.getKey();
73              String expression = propertyExpression.getValue();
74  
75              // if property name is nested, need to move the expression to the parent object
76              if (StringUtils.contains(propertyName, ".")) {
77                  boolean expressionMoved = moveNestedPropertyExpression(object, propertyName, expression);
78  
79                  // if expression moved, skip rest of control statement so it is not added to the adjusted map
80                  if (expressionMoved) {
81                      continue;
82                  }
83              }
84  
85              // replace the binding prefixes
86              String adjustedExpression = replaceBindingPrefixes(view, object, expression);
87  
88              adjustedPropertyExpressions.put(propertyName, adjustedExpression);
89          }
90  
91          // update property expressions map on object
92          ObjectPropertyUtils.setPropertyValue(object, UifPropertyPaths.PROPERTY_EXPRESSIONS,
93                  adjustedPropertyExpressions);
94      }
95  
96      /**
97       * Adjusts the property expressions for a given object
98       *
99       * <p>
100      * The org.kuali.rice.krad.uif.UifConstants#NO_BIND_ADJUST_PREFIX prefix will be removed
101      * as this is a placeholder indicating that the property is directly on the form.
102      * The org.kuali.rice.krad.uif.UifConstants#FIELD_PATH_BIND_ADJUST_PREFIX prefix will be replaced by
103      * the object's field path - this is only applicable to DataFields. The
104      * org.kuali.rice.krad.uif.UifConstants#DEFAULT_PATH_BIND_ADJUST_PREFIX prefix will be replaced
105      * by the view's default path if it is set.
106      * </p>
107      *
108      * @param view - the parent view of the object
109      * @param object - Object to adjust property expressions on
110      * @param expression - The expression to adjust
111      * @return the adjusted expression String
112      */
113     public static String replaceBindingPrefixes(View view, Object object, String expression) {
114         String adjustedExpression = StringUtils.replace(expression, UifConstants.NO_BIND_ADJUST_PREFIX, "");
115 
116         // replace the field path prefix for DataFields
117         if (object instanceof DataField) {
118 
119             // Get the binding path from the object
120             BindingInfo bindingInfo = ((DataField) object).getBindingInfo();
121             String fieldPath = bindingInfo.getBindingPath();
122 
123             // Remove the property name from the binding path
124             fieldPath = StringUtils.removeEnd(fieldPath, "." + bindingInfo.getBindingName());
125             adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.FIELD_PATH_BIND_ADJUST_PREFIX,
126                     fieldPath + ".");
127         } else {
128             adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.FIELD_PATH_BIND_ADJUST_PREFIX,
129                     "");
130         }
131 
132         // replace the default path prefix if there is one set on the view
133         if (StringUtils.isNotBlank(view.getDefaultBindingObjectPath())) {
134             adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.DEFAULT_PATH_BIND_ADJUST_PREFIX,
135                     view.getDefaultBindingObjectPath() + ".");
136 
137         } else {
138             adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.DEFAULT_PATH_BIND_ADJUST_PREFIX,
139                     "");
140         }
141 
142         // replace line path binding prefix with the actual line path
143         if (adjustedExpression.contains(UifConstants.LINE_PATH_BIND_ADJUST_PREFIX) && (object instanceof Component)) {
144             String linePath = getLinePathPrefixValue((Component) object);
145 
146             adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.LINE_PATH_BIND_ADJUST_PREFIX,
147                     linePath + ".");
148         }
149 
150         // replace node path binding prefix with the actual node path
151         if (adjustedExpression.contains(UifConstants.NODE_PATH_BIND_ADJUST_PREFIX) && (object instanceof Component)) {
152             String nodePath = "";
153 
154             Map<String, Object> context = ((Component) object).getContext();
155             if (context.containsKey(UifConstants.ContextVariableNames.NODE_PATH)) {
156                 nodePath = (String) context.get(UifConstants.ContextVariableNames.NODE_PATH);
157             }
158 
159             adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.NODE_PATH_BIND_ADJUST_PREFIX,
160                     nodePath + ".");
161         }
162 
163         return adjustedExpression;
164     }
165 
166     /**
167      * Determines the value for the org.kuali.rice.krad.uif.UifConstants#LINE_PATH_BIND_ADJUST_PREFIX binding prefix
168      * based on collection group found in the component context
169      *
170      * @param component - component instance for which the prefix is configured on
171      * @return String line binding path or empty string if path not found
172      */
173     protected static String getLinePathPrefixValue(Component component) {
174         String linePath = "";
175 
176         CollectionGroup collectionGroup = (CollectionGroup) (component.getContext().get(
177                 UifConstants.ContextVariableNames.COLLECTION_GROUP));
178         if (collectionGroup == null) {
179             return linePath;
180         }
181 
182         Object indexObj = component.getContext().get(UifConstants.ContextVariableNames.INDEX);
183         if (indexObj != null) {
184             int index = (Integer) indexObj;
185             boolean addLine = false;
186             Object addLineObj = component.getContext().get(UifConstants.ContextVariableNames.IS_ADD_LINE);
187 
188             if (addLineObj != null) {
189                 addLine = (Boolean) addLineObj;
190             }
191 
192             if (addLine) {
193                 linePath = collectionGroup.getAddLineBindingInfo().getBindingPath();
194             } else {
195                 linePath = collectionGroup.getBindingInfo().getBindingPath() + "[" + index + "]";
196             }
197         }
198 
199         return linePath;
200     }
201 
202     /**
203      * Moves any nested property expressions to the parent object
204      *
205      * @param object - the object containing the expression
206      * @param propertyName - the property the expression is on
207      * @param expression - the expression to move
208      * @return
209      */
210     protected static boolean moveNestedPropertyExpression(Object object, String propertyName, String expression) {
211         boolean moved = false;
212 
213         // get the parent object for the property
214         String parentPropertyName = StringUtils.substringBeforeLast(propertyName, ".");
215         String propertyNameInParent = StringUtils.substringAfterLast(propertyName, ".");
216 
217         Object parentObject = ObjectPropertyUtils.getPropertyValue(object, parentPropertyName);
218         if ((parentObject != null) && ObjectPropertyUtils.isReadableProperty(parentObject,
219                 UifPropertyPaths.PROPERTY_EXPRESSIONS) && ((parentObject instanceof Component)
220                 || (parentObject instanceof LayoutManager)
221                 || (parentObject instanceof BindingInfo))) {
222             Map<String, String> propertyExpressions = ObjectPropertyUtils.getPropertyValue(parentObject,
223                     UifPropertyPaths.PROPERTY_EXPRESSIONS);
224             if (propertyExpressions == null) {
225                 propertyExpressions = new HashMap<String, String>();
226             }
227 
228             // add expression to map on parent object
229             propertyExpressions.put(propertyNameInParent, expression);
230             ObjectPropertyUtils.setPropertyValue(parentObject, UifPropertyPaths.PROPERTY_EXPRESSIONS,
231                     propertyExpressions);
232             moved = true;
233         }
234 
235         return moved;
236     }
237 
238     /**
239      * Takes in an expression and a list to be filled in with names(property names)
240      * of controls found in the expression. This method returns a js expression which can
241      * be executed on the client to determine if the original exp was satisfied before
242      * interacting with the server - ie, this js expression is equivalent to the one passed in.
243      *
244      * There are limitations on the Spring expression language that can be used as this method.
245      * It is only used to parse expressions which are valid case statements for determining if
246      * some action/processing should be performed.  ONLY Properties, comparison operators, booleans,
247      * strings, matches expression, and boolean logic are supported.  Properties must
248      * be a valid property on the form, and should have a visible control within the view.
249      *
250      * Example valid exp: account.name == 'Account Name'
251      *
252      * @param exp
253      * @param controlNames
254      * @return
255      */
256     public static String parseExpression(String exp, List<String> controlNames) {
257         // clean up expression to ease parsing
258         exp = exp.trim();
259         if (exp.startsWith("@{")) {
260             exp = StringUtils.removeStart(exp, "@{");
261             if (exp.endsWith("}")) {
262                 exp = StringUtils.removeEnd(exp, "}");
263             }
264         }
265 
266         exp = StringUtils.replace(exp, "!=", " != ");
267         exp = StringUtils.replace(exp, "==", " == ");
268         exp = StringUtils.replace(exp, ">", " > ");
269         exp = StringUtils.replace(exp, "<", " < ");
270         exp = StringUtils.replace(exp, "<=", " <= ");
271         exp = StringUtils.replace(exp, ">=", " >= ");
272 
273         String conditionJs = exp;
274         String stack = "";
275 
276         boolean expectingSingleQuote = false;
277         boolean ignoreNext = false;
278         for (int i = 0; i < exp.length(); i++) {
279             char c = exp.charAt(i);
280             if (!expectingSingleQuote && !ignoreNext && (c == '(' || c == ' ' || c == ')')) {
281                 evaluateCurrentStack(stack.trim(), controlNames);
282                 //reset stack
283                 stack = "";
284                 continue;
285             } else if (!ignoreNext && c == '\'') {
286                 stack = stack + c;
287                 expectingSingleQuote = !expectingSingleQuote;
288             } else if (c == '\\') {
289                 stack = stack + c;
290                 ignoreNext = !ignoreNext;
291             } else {
292                 stack = stack + c;
293                 ignoreNext = false;
294             }
295         }
296 
297         if (StringUtils.isNotEmpty(stack)) {
298             evaluateCurrentStack(stack.trim(), controlNames);
299         }
300 
301         conditionJs = conditionJs.replaceAll("\\s(?i:ne)\\s", " != ").replaceAll("\\s(?i:eq)\\s", " == ").replaceAll(
302                 "\\s(?i:gt)\\s", " > ").replaceAll("\\s(?i:lt)\\s", " < ").replaceAll("\\s(?i:lte)\\s", " <= ")
303                 .replaceAll("\\s(?i:gte)\\s", " >= ").replaceAll("\\s(?i:and)\\s", " && ").replaceAll("\\s(?i:or)\\s",
304                         " || ").replaceAll("\\s(?i:not)\\s", " != ").replaceAll("\\s(?i:null)\\s?", " '' ").replaceAll(
305                         "\\s?(?i:#empty)\\((.*?)\\)", "isValueEmpty($1)");
306 
307         if (conditionJs.contains("matches")) {
308             conditionJs = conditionJs.replaceAll("\\s+(?i:matches)\\s+'.*'", ".match(/" + "$0" + "/) != null ");
309             conditionJs = conditionJs.replaceAll("\\(/\\s+(?i:matches)\\s+'", "(/");
310             conditionJs = conditionJs.replaceAll("'\\s*/\\)", "/)");
311         }
312 
313         for (String propertyName : controlNames) {
314             conditionJs = conditionJs.replace(propertyName, "coerceValue(\"" + propertyName + "\")");
315         }
316 
317         return conditionJs;
318     }
319 
320     /**
321      * Used internally by parseExpression to evalute if the current stack is a property
322      * name (ie, will be a control on the form)
323      *
324      * @param stack
325      * @param controlNames
326      */
327     public static void evaluateCurrentStack(String stack, List<String> controlNames) {
328         if (StringUtils.isNotBlank(stack)) {
329             if (!(stack.equals("==")
330                     || stack.equals("!=")
331                     || stack.equals(">")
332                     || stack.equals("<")
333                     || stack.equals(">=")
334                     || stack.equals("<=")
335                     || stack.equalsIgnoreCase("ne")
336                     || stack.equalsIgnoreCase("eq")
337                     || stack.equalsIgnoreCase("gt")
338                     || stack.equalsIgnoreCase("lt")
339                     || stack.equalsIgnoreCase("lte")
340                     || stack.equalsIgnoreCase("gte")
341                     || stack.equalsIgnoreCase("matches")
342                     || stack.equalsIgnoreCase("null")
343                     || stack.equalsIgnoreCase("false")
344                     || stack.equalsIgnoreCase("true")
345                     || stack.equalsIgnoreCase("and")
346                     || stack.equalsIgnoreCase("or")
347                     || stack.contains("#empty")
348                     || stack.startsWith("'")
349                     || stack.endsWith("'"))) {
350 
351                 boolean isNumber = false;
352                 if ((StringUtils.isNumeric(stack.substring(0, 1)) || stack.substring(0, 1).equals("-"))) {
353                     try {
354                         Double.parseDouble(stack);
355                         isNumber = true;
356                     } catch (NumberFormatException e) {
357                         isNumber = false;
358                     }
359                 }
360 
361                 if (!(isNumber)) {
362                     if (!controlNames.contains(stack)) {
363                         controlNames.add(stack);
364                     }
365                 }
366             }
367         }
368     }
369 
370 }