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 (object instanceof Component) {
144             String linePath = getLinePathPrefixValue((Component) object);
145 
146             if (StringUtils.isNotEmpty(linePath)) {
147                 adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.LINE_PATH_BIND_ADJUST_PREFIX,
148                         linePath + ".");
149             }
150         }
151 
152         return adjustedExpression;
153     }
154 
155     /**
156      * Determines the value for the org.kuali.rice.krad.uif.UifConstants#LINE_PATH_BIND_ADJUST_PREFIX binding prefix
157      * based on collection group found in the component context
158      *
159      * @param component - component instance for which the prefix is configured on
160      * @return String line binding path or empty string if path not found
161      */
162     protected static String getLinePathPrefixValue(Component component) {
163         String linePath = "";
164 
165         CollectionGroup collectionGroup = (CollectionGroup) (component.getContext().get(
166                 UifConstants.ContextVariableNames.COLLECTION_GROUP));
167         if (collectionGroup == null) {
168             return linePath;
169         }
170 
171         Object indexObj = component.getContext().get(UifConstants.ContextVariableNames.INDEX);
172         if (indexObj != null) {
173             int index = (Integer) indexObj;
174             boolean addLine = false;
175             Object addLineObj = component.getContext().get(UifConstants.ContextVariableNames.IS_ADD_LINE);
176 
177             if (addLineObj != null) {
178                 addLine = (Boolean) addLineObj;
179             }
180 
181             if (addLine) {
182                 linePath = collectionGroup.getAddLineBindingInfo().getBindingPath();
183             } else {
184                 linePath = collectionGroup.getBindingInfo().getBindingPath() + "[" + index + "]";
185             }
186         }
187 
188         return linePath;
189     }
190 
191     /**
192      * Moves any nested property expressions to the parent object
193      *
194      * @param object - the object containing the expression
195      * @param propertyName - the property the expression is on
196      * @param expression - the epxression
197      * @return
198      */
199     protected static boolean moveNestedPropertyExpression(Object object, String propertyName, String expression) {
200         boolean moved = false;
201 
202         // get the parent object for the property
203         String parentPropertyName = StringUtils.substringBeforeLast(propertyName, ".");
204         String propertyNameInParent = StringUtils.substringAfterLast(propertyName, ".");
205 
206         Object parentObject = ObjectPropertyUtils.getPropertyValue(object, parentPropertyName);
207         if ((parentObject != null) && ObjectPropertyUtils.isReadableProperty(parentObject,
208                 UifPropertyPaths.PROPERTY_EXPRESSIONS) && ((parentObject instanceof Component)
209                 || (parentObject instanceof LayoutManager)
210                 || (parentObject instanceof BindingInfo))) {
211             Map<String, String> propertyExpressions = ObjectPropertyUtils.getPropertyValue(parentObject,
212                     UifPropertyPaths.PROPERTY_EXPRESSIONS);
213             if (propertyExpressions == null) {
214                 propertyExpressions = new HashMap<String, String>();
215             }
216 
217             // add expression to map on parent object
218             propertyExpressions.put(propertyNameInParent, expression);
219             ObjectPropertyUtils.setPropertyValue(parentObject, UifPropertyPaths.PROPERTY_EXPRESSIONS,
220                     propertyExpressions);
221             moved = true;
222         }
223 
224         return moved;
225     }
226 
227     /**
228      * Takes in an expression and a list to be filled in with names(property names)
229      * of controls found in the expression. This method returns a js expression which can
230      * be executed on the client to determine if the original exp was satisfied before
231      * interacting with the server - ie, this js expression is equivalent to the one passed in.
232      *
233      * There are limitations on the Spring expression language that can be used as this method.
234      * It is only used to parse expressions which are valid case statements for determining if
235      * some action/processing should be performed.  ONLY Properties, comparison operators, booleans,
236      * strings, matches expression, and boolean logic are supported.  Properties must
237      * be a valid property on the form, and should have a visible control within the view.
238      *
239      * Example valid exp: account.name == 'Account Name'
240      *
241      * @param exp
242      * @param controlNames
243      * @return
244      */
245     public static String parseExpression(String exp, List<String> controlNames) {
246         // clean up expression to ease parsing
247         exp = exp.trim();
248         if (exp.startsWith("@{")) {
249             exp = StringUtils.removeStart(exp, "@{");
250             if (exp.endsWith("}")) {
251                 exp = StringUtils.removeEnd(exp, "}");
252             }
253         }
254 
255         exp = StringUtils.replace(exp, "!=", " != ");
256         exp = StringUtils.replace(exp, "==", " == ");
257         exp = StringUtils.replace(exp, ">", " > ");
258         exp = StringUtils.replace(exp, "<", " < ");
259         exp = StringUtils.replace(exp, "<=", " <= ");
260         exp = StringUtils.replace(exp, ">=", " >= ");
261 
262         String conditionJs = exp;
263         String stack = "";
264 
265         boolean expectingSingleQuote = false;
266         boolean ignoreNext = false;
267         for (int i = 0; i < exp.length(); i++) {
268             char c = exp.charAt(i);
269             if (!expectingSingleQuote && !ignoreNext && (c == '(' || c == ' ' || c == ')')) {
270                 evaluateCurrentStack(stack.trim(), controlNames);
271                 //reset stack
272                 stack = "";
273                 continue;
274             } else if (!ignoreNext && c == '\'') {
275                 stack = stack + c;
276                 expectingSingleQuote = !expectingSingleQuote;
277             } else if (c == '\\') {
278                 stack = stack + c;
279                 ignoreNext = !ignoreNext;
280             } else {
281                 stack = stack + c;
282                 ignoreNext = false;
283             }
284         }
285 
286         if (StringUtils.isNotEmpty(stack)) {
287             evaluateCurrentStack(stack.trim(), controlNames);
288         }
289 
290         conditionJs = conditionJs.replaceAll("\\s(?i:ne)\\s", " != ").replaceAll("\\s(?i:eq)\\s", " == ").replaceAll(
291                 "\\s(?i:gt)\\s", " > ").replaceAll("\\s(?i:lt)\\s", " < ").replaceAll("\\s(?i:lte)\\s", " <= ")
292                 .replaceAll("\\s(?i:gte)\\s", " >= ").replaceAll("\\s(?i:and)\\s", " && ").replaceAll("\\s(?i:or)\\s",
293                         " || ").replaceAll("\\s(?i:not)\\s", " != ").replaceAll("\\s(?i:null)\\s?", " '' ").replaceAll(
294                         "\\s?(?i:#empty)\\((.*?)\\)", "isValueEmpty($1)");
295 
296         if (conditionJs.contains("matches")) {
297             conditionJs = conditionJs.replaceAll("\\s+(?i:matches)\\s+'.*'", ".match(/" + "$0" + "/) != null ");
298             conditionJs = conditionJs.replaceAll("\\(/\\s+(?i:matches)\\s+'", "(/");
299             conditionJs = conditionJs.replaceAll("'\\s*/\\)", "/)");
300         }
301 
302         for (String propertyName : controlNames) {
303             conditionJs = conditionJs.replace(propertyName, "coerceValue(\"" + propertyName + "\")");
304         }
305 
306         return conditionJs;
307     }
308 
309     /**
310      * Used internally by parseExpression to evalute if the current stack is a property
311      * name (ie, will be a control on the form)
312      *
313      * @param stack
314      * @param controlNames
315      */
316     public static void evaluateCurrentStack(String stack, List<String> controlNames) {
317         if (StringUtils.isNotBlank(stack)) {
318             if (!(stack.equals("==")
319                     || stack.equals("!=")
320                     || stack.equals(">")
321                     || stack.equals("<")
322                     || stack.equals(">=")
323                     || stack.equals("<=")
324                     || stack.equalsIgnoreCase("ne")
325                     || stack.equalsIgnoreCase("eq")
326                     || stack.equalsIgnoreCase("gt")
327                     || stack.equalsIgnoreCase("lt")
328                     || stack.equalsIgnoreCase("lte")
329                     || stack.equalsIgnoreCase("gte")
330                     || stack.equalsIgnoreCase("matches")
331                     || stack.equalsIgnoreCase("null")
332                     || stack.equalsIgnoreCase("false")
333                     || stack.equalsIgnoreCase("true")
334                     || stack.equalsIgnoreCase("and")
335                     || stack.equalsIgnoreCase("or")
336                     || stack.contains("#empty")
337                     || stack.startsWith("'")
338                     || stack.endsWith("'"))) {
339 
340                 boolean isNumber = false;
341                 if ((StringUtils.isNumeric(stack.substring(0, 1)) || stack.substring(0, 1).equals("-"))) {
342                     try {
343                         Double.parseDouble(stack);
344                         isNumber = true;
345                     } catch (NumberFormatException e) {
346                         isNumber = false;
347                     }
348                 }
349 
350                 if (!(isNumber)) {
351                     if (!controlNames.contains(stack)) {
352                         controlNames.add(stack);
353                     }
354                 }
355             }
356         }
357     }
358 
359 }