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.service.impl;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.kuali.rice.krad.datadictionary.uif.UifDictionaryBean;
20  import org.kuali.rice.krad.uif.UifConstants;
21  import org.kuali.rice.krad.uif.component.BindingInfo;
22  import org.kuali.rice.krad.uif.component.Component;
23  import org.kuali.rice.krad.uif.component.KeepExpression;
24  import org.kuali.rice.krad.uif.component.PropertyReplacer;
25  import org.kuali.rice.krad.uif.container.CollectionGroup;
26  import org.kuali.rice.krad.uif.field.DataField;
27  import org.kuali.rice.krad.uif.layout.LayoutManager;
28  import org.kuali.rice.krad.uif.service.ExpressionEvaluatorService;
29  import org.kuali.rice.krad.uif.util.CloneUtils;
30  import org.kuali.rice.krad.uif.util.ExpressionFunctions;
31  import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
32  import org.kuali.rice.krad.uif.view.View;
33  import org.springframework.expression.Expression;
34  import org.springframework.expression.ExpressionParser;
35  import org.springframework.expression.common.TemplateParserContext;
36  import org.springframework.expression.spel.standard.SpelExpressionParser;
37  import org.springframework.expression.spel.support.StandardEvaluationContext;
38  
39  import java.util.Collection;
40  import java.util.List;
41  import java.util.Map;
42  import java.util.Map.Entry;
43  
44  /**
45   * Evaluates expression language statements using the Spring EL engine
46   *
47   * @author Kuali Rice Team (rice.collab@kuali.org)
48   */
49  public class ExpressionEvaluatorServiceImpl implements ExpressionEvaluatorService {
50      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(
51              ExpressionEvaluatorServiceImpl.class);
52  
53      /**
54       * @see org.kuali.rice.krad.uif.service.ExpressionEvaluatorService#evaluateExpressionsOnConfigurable(org.kuali.rice.krad.uif.view.View,
55       *      org.kuali.rice.krad.datadictionary.uif.UifDictionaryBean, java.lang.Object,
56       *      java.util.Map<java.lang.String,java.lang.Object>)
57       */
58      public void evaluateExpressionsOnConfigurable(View view, UifDictionaryBean expressionConfigurable,
59              Object contextObject, Map<String, Object> evaluationParameters) {
60          if ((expressionConfigurable instanceof Component) || (expressionConfigurable instanceof LayoutManager)) {
61              evaluatePropertyReplacers(view, expressionConfigurable, contextObject, evaluationParameters);
62          }
63          evaluatePropertyExpressions(view, expressionConfigurable, contextObject, evaluationParameters);
64      }
65  
66      /**
67       * @see org.kuali.rice.krad.uif.service.ExpressionEvaluatorService#evaluateExpressionTemplate(java.lang.Object,
68       *      java.util.Map, java.lang.String)
69       */
70      public String evaluateExpressionTemplate(Object contextObject, Map<String, Object> evaluationParameters,
71              String expressionTemplate) {
72          StandardEvaluationContext context = new StandardEvaluationContext(contextObject);
73          context.setVariables(evaluationParameters);
74          addCustomFunctions(context);
75  
76          ExpressionParser parser = new SpelExpressionParser();
77  
78          String result = null;
79          try {
80              Expression expression = null;
81              if (StringUtils.contains(expressionTemplate, UifConstants.EL_PLACEHOLDER_PREFIX)) {
82                  expression = parser.parseExpression(expressionTemplate, new TemplateParserContext(
83                          UifConstants.EL_PLACEHOLDER_PREFIX, UifConstants.EL_PLACEHOLDER_SUFFIX));
84              } else {
85                  expression = parser.parseExpression(expressionTemplate);
86              }
87  
88              result = expression.getValue(context, String.class);
89          } catch (Exception e) {
90              LOG.error("Exception evaluating expression: " + expressionTemplate);
91              throw new RuntimeException("Exception evaluating expression: " + expressionTemplate, e);
92          }
93  
94          return result;
95      }
96  
97      /**
98       * @see org.kuali.rice.krad.uif.service.ExpressionEvaluatorService#evaluatePropertyExpression(
99       * org.kuali.rice.krad.uif.view.View, Object, java.util.Map<String,Object>,
100      * org.kuali.rice.krad.datadictionary.uif.UifDictionaryBean, String, boolean)
101      */
102     public void evaluatePropertyExpression(View view, Object contextObject, Map<String, Object> evaluationParameters,
103             UifDictionaryBean expressionConfigurable, String propertyName, boolean removeExpression) {
104 
105         Map<String, String> propertyExpressions = expressionConfigurable.getPropertyExpressions();
106         if ((propertyExpressions == null) || !propertyExpressions.containsKey(propertyName)) {
107             return;
108         }
109 
110         String expression = propertyExpressions.get(propertyName);
111 
112         // check whether expression should be evaluated or property should retain the expression
113         if (CloneUtils.fieldHasAnnotation(expressionConfigurable.getClass(), propertyName, KeepExpression.class)) {
114             // set expression as property value to be handled by the component
115             ObjectPropertyUtils.setPropertyValue(expressionConfigurable, propertyName, expression);
116             return;
117         }
118 
119         Object propertyValue = null;
120 
121         // replace binding prefixes (lp, dp, fp) in expression before evaluation
122         String adjustedExpression = replaceBindingPrefixes(view, expressionConfigurable, expression);
123 
124         // determine whether the expression is a string template, or evaluates to another object type
125         if (StringUtils.startsWith(adjustedExpression, UifConstants.EL_PLACEHOLDER_PREFIX) && StringUtils.endsWith(
126                 adjustedExpression, UifConstants.EL_PLACEHOLDER_SUFFIX) && (StringUtils.countMatches(adjustedExpression,
127                 UifConstants.EL_PLACEHOLDER_PREFIX) == 1)) {
128             propertyValue = evaluateExpression(contextObject, evaluationParameters, adjustedExpression);
129         } else {
130             // treat as string template
131             propertyValue = evaluateExpressionTemplate(contextObject, evaluationParameters, adjustedExpression);
132         }
133 
134         // if property name has the special indicator then we need to add the expression result to the property
135         // value instead of replace
136         if (StringUtils.endsWith(propertyName, ExpressionEvaluatorService.EMBEDDED_PROPERTY_NAME_ADD_INDICATOR)) {
137             StringUtils.removeEnd(propertyName, ExpressionEvaluatorService.EMBEDDED_PROPERTY_NAME_ADD_INDICATOR);
138 
139             Collection collectionValue = ObjectPropertyUtils.getPropertyValue(expressionConfigurable, propertyName);
140             if (collectionValue == null) {
141                 throw new RuntimeException("Property name: "
142                         + propertyName
143                         + " with collection type was not initialized. Cannot add expression result");
144             }
145             collectionValue.add(propertyValue);
146         } else {
147             ObjectPropertyUtils.setPropertyValue(expressionConfigurable, propertyName, propertyValue);
148         }
149 
150         if (removeExpression) {
151             propertyExpressions.remove(propertyName);
152         }
153     }
154 
155     /**
156      * @see org.kuali.rice.krad.uif.service.ExpressionEvaluatorService#containsElPlaceholder(String)
157      */
158     public boolean containsElPlaceholder(String value) {
159         boolean containsElPlaceholder = false;
160 
161         if (StringUtils.isNotBlank(value)) {
162             String elPlaceholder = StringUtils.substringBetween(value, UifConstants.EL_PLACEHOLDER_PREFIX,
163                     UifConstants.EL_PLACEHOLDER_SUFFIX);
164             if (StringUtils.isNotBlank(elPlaceholder)) {
165                 containsElPlaceholder = true;
166             }
167         }
168 
169         return containsElPlaceholder;
170     }
171 
172     /**
173      * @see org.kuali.rice.krad.uif.service.ExpressionEvaluatorService#replaceBindingPrefixes(org.kuali.rice.krad.uif.view.View,
174      *      Object, String)
175      */
176     public String replaceBindingPrefixes(View view, Object object, String expression) {
177         String adjustedExpression = StringUtils.replace(expression, UifConstants.NO_BIND_ADJUST_PREFIX, "");
178 
179         // replace the field path prefix for DataFields
180         if (object instanceof DataField) {
181 
182             // Get the binding path from the object
183             BindingInfo bindingInfo = ((DataField) object).getBindingInfo();
184             String fieldPath = bindingInfo.getBindingPath();
185 
186             // Remove the property name from the binding path
187             fieldPath = StringUtils.removeEnd(fieldPath, "." + bindingInfo.getBindingName());
188             adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.FIELD_PATH_BIND_ADJUST_PREFIX,
189                     fieldPath + ".");
190         } else {
191             adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.FIELD_PATH_BIND_ADJUST_PREFIX,
192                     "");
193         }
194 
195         // replace the default path prefix if there is one set on the view
196         if (StringUtils.isNotBlank(view.getDefaultBindingObjectPath())) {
197             adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.DEFAULT_PATH_BIND_ADJUST_PREFIX,
198                     view.getDefaultBindingObjectPath() + ".");
199 
200         } else {
201             adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.DEFAULT_PATH_BIND_ADJUST_PREFIX,
202                     "");
203         }
204 
205         // replace line path binding prefix with the actual line path
206         if (adjustedExpression.contains(UifConstants.LINE_PATH_BIND_ADJUST_PREFIX) && (object instanceof Component)) {
207             String linePath = getLinePathPrefixValue((Component) object);
208 
209             adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.LINE_PATH_BIND_ADJUST_PREFIX,
210                     linePath + ".");
211         }
212 
213         // replace node path binding prefix with the actual node path
214         if (adjustedExpression.contains(UifConstants.NODE_PATH_BIND_ADJUST_PREFIX) && (object instanceof Component)) {
215             String nodePath = "";
216 
217             Map<String, Object> context = ((Component) object).getContext();
218             if (context.containsKey(UifConstants.ContextVariableNames.NODE_PATH)) {
219                 nodePath = (String) context.get(UifConstants.ContextVariableNames.NODE_PATH);
220             }
221 
222             adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.NODE_PATH_BIND_ADJUST_PREFIX,
223                     nodePath + ".");
224         }
225 
226         return adjustedExpression;
227     }
228 
229     /**
230      * @see org.kuali.rice.krad.uif.service.ExpressionEvaluatorService#evaluateExpression(java.lang.Object,
231      *      java.util.Map, java.lang.String)
232      */
233     public Object evaluateExpression(Object contextObject, Map<String, Object> evaluationParameters,
234             String expressionStr) {
235         StandardEvaluationContext context = new StandardEvaluationContext(contextObject);
236         context.setVariables(evaluationParameters);
237         addCustomFunctions(context);
238 
239         // if expression contains placeholders remove before evaluating
240         if (StringUtils.startsWith(expressionStr, UifConstants.EL_PLACEHOLDER_PREFIX) && StringUtils.endsWith(
241                 expressionStr, UifConstants.EL_PLACEHOLDER_SUFFIX)) {
242             expressionStr = StringUtils.removeStart(expressionStr, UifConstants.EL_PLACEHOLDER_PREFIX);
243             expressionStr = StringUtils.removeEnd(expressionStr, UifConstants.EL_PLACEHOLDER_SUFFIX);
244         }
245 
246         ExpressionParser parser = new SpelExpressionParser();
247         Object result = null;
248         try {
249             Expression expression = parser.parseExpression(expressionStr);
250 
251             result = expression.getValue(context);
252         } catch (Exception e) {
253             LOG.error("Exception evaluating expression: " + expressionStr);
254             throw new RuntimeException("Exception evaluating expression: " + expressionStr, e);
255         }
256 
257         return result;
258     }
259 
260     /**
261      * Registers custom functions for el expressions with the given context
262      *
263      * @param context - context instance to register functions to
264      */
265     protected void addCustomFunctions(StandardEvaluationContext context) {
266         try {
267             // TODO: possibly reflect ExpressionFunctions and add automatically
268             context.registerFunction("isAssignableFrom", ExpressionFunctions.class.getDeclaredMethod("isAssignableFrom",
269                     new Class[]{Class.class, Class.class}));
270             context.registerFunction("empty", ExpressionFunctions.class.getDeclaredMethod("empty",
271                     new Class[]{Object.class}));
272             context.registerFunction("emptyList", ExpressionFunctions.class.getDeclaredMethod("emptyList",
273                     new Class[]{List.class}));
274             context.registerFunction("listContains", ExpressionFunctions.class.getDeclaredMethod("listContains",
275                     new Class[]{List.class, Object[].class}));
276             context.registerFunction("getName", ExpressionFunctions.class.getDeclaredMethod("getName",
277                     new Class[]{Class.class}));
278             context.registerFunction("getParm", ExpressionFunctions.class.getDeclaredMethod("getParm",
279                     new Class[]{String.class, String.class, String.class}));
280             context.registerFunction("getParmInd", ExpressionFunctions.class.getDeclaredMethod("getParmInd",
281                     new Class[]{String.class, String.class, String.class}));
282             context.registerFunction("hasPerm", ExpressionFunctions.class.getDeclaredMethod("hasPerm",
283                     new Class[]{String.class, String.class}));
284             context.registerFunction("hasPermDtls", ExpressionFunctions.class.getDeclaredMethod("hasPermDtls",
285                     new Class[]{String.class, String.class, Map.class, Map.class}));
286             context.registerFunction("hasPermTmpl", ExpressionFunctions.class.getDeclaredMethod("hasPermTmpl",
287                     new Class[]{String.class, String.class, Map.class, Map.class}));
288             context.registerFunction("sequence", ExpressionFunctions.class.getDeclaredMethod("sequence",
289                     new Class[]{String.class}));
290         } catch (NoSuchMethodException e) {
291             LOG.error("Custom function for el expressions not found: " + e.getMessage());
292             throw new RuntimeException("Custom function for el expressions not found: " + e.getMessage(), e);
293         }
294     }
295 
296     /**
297      * Iterates through any configured <code>PropertyReplacer</code> instances for the component and
298      * evaluates the given condition. If the condition is met, the replacement value is set on the
299      * corresponding property
300      *
301      * @param view - view instance being rendered
302      * @param expressionConfigurable - expressionConfigurable instance with property replacers list, should be either a
303      * component or layout
304      * manager
305      * @param contextObject - context for el evaluation
306      * @param evaluationParameters - parameters for el evaluation
307      */
308     protected void evaluatePropertyReplacers(View view, UifDictionaryBean expressionConfigurable, Object contextObject,
309             Map<String, Object> evaluationParameters) {
310         List<PropertyReplacer> replacers = null;
311         if (Component.class.isAssignableFrom(expressionConfigurable.getClass())) {
312             replacers = ((Component) expressionConfigurable).getPropertyReplacers();
313         } else if (LayoutManager.class.isAssignableFrom(expressionConfigurable.getClass())) {
314             replacers = ((LayoutManager) expressionConfigurable).getPropertyReplacers();
315         }
316 
317         for (PropertyReplacer propertyReplacer : replacers) {
318             String expression = propertyReplacer.getCondition();
319             String adjustedExpression = replaceBindingPrefixes(view, expressionConfigurable, expression);
320 
321             String conditionEvaluation = evaluateExpressionTemplate(contextObject, evaluationParameters,
322                     adjustedExpression);
323             boolean conditionSuccess = Boolean.parseBoolean(conditionEvaluation);
324             if (conditionSuccess) {
325                 ObjectPropertyUtils.setPropertyValue(expressionConfigurable, propertyReplacer.getPropertyName(),
326                         propertyReplacer.getReplacement());
327             }
328         }
329     }
330 
331     /**
332      * Iterates through the keys of the property expressions map and invokes
333      * {@link #evaluatePropertyExpression(org.kuali.rice.krad.uif.view.View, Object, java.util.Map,
334      * org.kuali.rice.krad.datadictionary.uif.UifDictionaryBean, String, boolean)}
335      *
336      * <p>
337      * If the expression is an el template (part static text and part expression), only the expression
338      * part will be replaced with the result. More than one expressions may be contained within the template
339      * </p>
340      *
341      * @param view - view instance that is being rendered
342      * @param expressionConfigurable - object instance to evaluate expressions for
343      * @param contextObject - object providing the default context for expressions
344      * @param evaluationParameters - map of additional parameters that may be used within the expressions
345      */
346     protected void evaluatePropertyExpressions(View view, UifDictionaryBean expressionConfigurable,
347             Object contextObject, Map<String, Object> evaluationParameters) {
348         Map<String, String> propertyExpressions = expressionConfigurable.getPropertyExpressions();
349         for (String propertyName : propertyExpressions.keySet()) {
350             evaluatePropertyExpression(view, contextObject, evaluationParameters, expressionConfigurable, propertyName,
351                     false);
352         }
353     }
354 
355     /**
356      * Determines the value for the {@link org.kuali.rice.krad.uif.UifConstants#LINE_PATH_BIND_ADJUST_PREFIX} binding
357      * prefix
358      * based on collection group found in the component context
359      *
360      * @param component - component instance for which the prefix is configured on
361      * @return String line binding path or empty string if path not found
362      */
363     protected static String getLinePathPrefixValue(Component component) {
364         String linePath = "";
365 
366         CollectionGroup collectionGroup = (CollectionGroup) (component.getContext().get(
367                 UifConstants.ContextVariableNames.COLLECTION_GROUP));
368         if (collectionGroup == null) {
369             LOG.warn("collection group not found for " + component + "," + component.getId() + ", " + component
370                     .getComponentTypeName());
371             return linePath;
372         }
373 
374         Object indexObj = component.getContext().get(UifConstants.ContextVariableNames.INDEX);
375         if (indexObj != null) {
376             int index = (Integer) indexObj;
377             boolean addLine = false;
378             Object addLineObj = component.getContext().get(UifConstants.ContextVariableNames.IS_ADD_LINE);
379 
380             if (addLineObj != null) {
381                 addLine = (Boolean) addLineObj;
382             }
383 
384             if (addLine) {
385                 linePath = collectionGroup.getAddLineBindingInfo().getBindingPath();
386             } else {
387                 linePath = collectionGroup.getBindingInfo().getBindingPath() + "[" + index + "]";
388             }
389         }
390 
391         return linePath;
392     }
393 
394 }