View Javadoc
1   /**
2    * Copyright 2005-2014 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.view;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.apache.commons.lang.math.NumberUtils;
20  import org.kuali.rice.core.api.exception.RiceRuntimeException;
21  import org.kuali.rice.krad.datadictionary.uif.UifDictionaryBean;
22  import org.kuali.rice.krad.uif.UifConstants;
23  import org.kuali.rice.krad.uif.component.BindingInfo;
24  import org.kuali.rice.krad.uif.component.Component;
25  import org.kuali.rice.krad.uif.component.KeepExpression;
26  import org.kuali.rice.krad.uif.component.PropertyReplacer;
27  import org.kuali.rice.krad.uif.container.CollectionGroup;
28  import org.kuali.rice.krad.uif.field.DataField;
29  import org.kuali.rice.krad.uif.layout.LayoutManager;
30  import org.kuali.rice.krad.uif.util.CopyUtils;
31  import org.kuali.rice.krad.uif.util.ExpressionFunctions;
32  import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
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.lang.reflect.Method;
40  import java.util.ArrayList;
41  import java.util.Collection;
42  import java.util.HashMap;
43  import java.util.List;
44  import java.util.Map;
45  import java.util.regex.Matcher;
46  import java.util.regex.Pattern;
47  
48  /**
49   * Evaluates expression language statements using the Spring EL engine
50   *
51   * @author Kuali Rice Team (rice.collab@kuali.org)
52   */
53  public class DefaultExpressionEvaluator implements ExpressionEvaluator {
54  
55      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(
56              DefaultExpressionEvaluator.class);
57  
58      protected static final Pattern SERVER_EVALUATION_PATTERN = Pattern.compile(
59              "(\\s?!?\\b(#|get|is)(.*?\\(.*?\\)))(\\s|$)");
60  
61      private StandardEvaluationContext evaluationContext;
62  
63      private Map<String, Expression> cachedExpressions;
64  
65      protected static ExpressionParser parser = new SpelExpressionParser();
66  
67      private static Method isAssignableFrom;
68      private static Method empty;
69      private static Method emptyList;
70      private static Method getService;
71      private static Method listContains;
72      private static Method getName;
73      private static Method getParam;
74      private static Method getParamAsBoolean;
75      private static Method hasPerm;
76      private static Method hasPermDtls;
77      private static Method hasPermTmpl;
78      private static Method sequence;
79      private static Method getDataObjectKey;
80      private static Method isProductionEnvironment;
81  
82      static {
83          try {
84              isAssignableFrom = ExpressionFunctions.class.getDeclaredMethod("isAssignableFrom",
85                      new Class[] {Class.class, Class.class});
86              empty = ExpressionFunctions.class.getDeclaredMethod("empty", new Class[] {Object.class});
87              emptyList = ExpressionFunctions.class.getDeclaredMethod("emptyList", new Class[] {List.class});
88              listContains = ExpressionFunctions.class.getDeclaredMethod("listContains",
89                      new Class[] {List.class, Object[].class});
90              getName = ExpressionFunctions.class.getDeclaredMethod("getName", new Class[] {Class.class});
91              getParam = ExpressionFunctions.class.getDeclaredMethod("getParam",
92                      new Class[] {String.class, String.class, String.class});
93              getParamAsBoolean = ExpressionFunctions.class.getDeclaredMethod("getParamAsBoolean",
94                      new Class[] {String.class, String.class, String.class});
95              hasPerm = ExpressionFunctions.class.getDeclaredMethod("hasPerm", new Class[] {String.class, String.class});
96              hasPermDtls = ExpressionFunctions.class.getDeclaredMethod("hasPermDtls",
97                      new Class[] {String.class, String.class, Map.class, Map.class});
98              hasPermTmpl = ExpressionFunctions.class.getDeclaredMethod("hasPermTmpl",
99                      new Class[] {String.class, String.class, Map.class, Map.class});
100             getService = ExpressionFunctions.class.getDeclaredMethod("getService", new Class[] {String.class});
101             sequence = ExpressionFunctions.class.getDeclaredMethod("sequence", new Class[] {String.class});
102             getDataObjectKey = ExpressionFunctions.class.getDeclaredMethod("getDataObjectKey",
103                     new Class[] {String.class});
104             isProductionEnvironment = ExpressionFunctions.class.getDeclaredMethod("isProductionEnvironment", null);
105         } catch (NoSuchMethodException e) {
106             LOG.error("Custom function for el expressions not found: " + e.getMessage());
107             throw new RuntimeException("Custom function for el expressions not found: " + e.getMessage(), e);
108         }
109     }
110 
111     /**
112      * Default constructor
113      */
114     public DefaultExpressionEvaluator() {
115         cachedExpressions = new HashMap<String, Expression>();
116     }
117 
118     /**
119      * {@inheritDoc}
120      */
121     @Override
122     public void populatePropertyExpressionsFromGraph(UifDictionaryBean expressionConfigurable,
123             boolean buildRefreshGraphs) {
124         if (expressionConfigurable == null || expressionConfigurable.getExpressionGraph() == null) {
125             return;
126         }
127 
128         Map<String, String> expressionGraph = expressionConfigurable.getExpressionGraph();
129         for (Map.Entry<String, String> expressionEntry : expressionGraph.entrySet()) {
130             String propertyName = expressionEntry.getKey();
131             String expression = expressionEntry.getValue();
132 
133             // by default assume expression belongs with passed in expressionConfigurable
134             UifDictionaryBean configurableWithExpression = expressionConfigurable;
135 
136             // if property name is nested, we need to move the expression to the last expressionConfigurable
137             String adjustedPropertyName = propertyName;
138             if (StringUtils.contains(propertyName, ".")) {
139                 String configurablePath = StringUtils.substringBeforeLast(propertyName, ".");
140                 adjustedPropertyName = StringUtils.substringAfterLast(propertyName, ".");
141 
142                 Object nestedObject = ObjectPropertyUtils.getPropertyValue(expressionConfigurable, configurablePath);
143                 if ((nestedObject == null) || !(nestedObject instanceof UifDictionaryBean)) {
144                     throw new RiceRuntimeException(
145                             "Object for which expression is configured on is null or does not implement UifDictionaryBean: '"
146                                     + configurablePath + "'");
147                 }
148 
149                 // use nested object as the expressionConfigurable which will get the property expression
150                 configurableWithExpression = (UifDictionaryBean) nestedObject;
151             }
152 
153             configurableWithExpression.getPropertyExpressions().put(adjustedPropertyName, expression);
154         }
155     }
156 
157     /**
158      * {@inheritDoc}
159      */
160     @Override
161     public String parseExpression(String exp, List<String> controlNames, Map<String, Object> context) {
162         // clean up expression to ease parsing
163         exp = cleanUpExpression(exp);
164 
165         // Evaluate server side method calls and constants, and place them in a map to be replaced later
166         Map<String, String> serverEvaluations = evaluateServerSideVariables(exp, context);
167 
168         String conditionJs = exp;
169         controlNames.addAll(findControlNamesInExpression(exp));
170 
171         // Replace all known accepted strings with javascript equivalent
172         conditionJs = replaceWithJsEquivalents(conditionJs);
173 
174         // Replace server evaluations in js string with evaluated value
175         for (String serverEvalToken : serverEvaluations.keySet()) {
176             String evaluatedValue = serverEvaluations.get(serverEvalToken);
177             conditionJs = conditionJs.replace(serverEvalToken, evaluatedValue);
178         }
179 
180         List<String> removeControlNames = new ArrayList<String>();
181         List<String> addControlNames = new ArrayList<String>();
182 
183         //convert property names to use coerceValue function and convert arrays to js arrays
184         for (String propertyName : controlNames) {
185             //array definitions are caught in controlNames because of the nature of the parse - convert them and remove
186             if (propertyName.trim().startsWith("{") && propertyName.trim().endsWith("}")) {
187                 String array = propertyName.trim().replace('{', '[');
188                 array = array.replace('}', ']');
189                 conditionJs = conditionJs.replace(propertyName, array);
190                 removeControlNames.add(propertyName);
191                 continue;
192             }
193 
194             //handle not
195             if (propertyName.startsWith("!")) {
196                 String actualPropertyName = StringUtils.removeStart(propertyName, "!");
197                 conditionJs = conditionJs.replace(propertyName, "!coerceValue(\"" + actualPropertyName + "\")");
198                 removeControlNames.add(propertyName);
199                 addControlNames.add(actualPropertyName);
200             } else {
201                 conditionJs = conditionJs.replace(propertyName, "coerceValue(\"" + propertyName + "\")");
202             }
203         }
204 
205         controlNames.removeAll(removeControlNames);
206         controlNames.addAll(addControlNames);
207 
208         // Simple short circuit logic below
209         boolean complexCondition = conditionJs.contains(" (") || conditionJs.startsWith("(");
210 
211         // Always remove AND'ed true
212         if (conditionJs.contains("true && ") || conditionJs.contains(" && true")) {
213             conditionJs = conditionJs.replace(" && true", "");
214             conditionJs = conditionJs.replace("true && ", "");
215         }
216 
217         // An AND'ed false, or an OR'ed true, or true/false by themselves will always evaluate to the same outcome
218         // in a simple condition, so no need for client evaluation (server will handle the evaluation)
219         if (!complexCondition && (conditionJs.contains("false &&")) || conditionJs.contains("&& false") || conditionJs
220                 .contains("|| true") || conditionJs.contains("true ||") || conditionJs.equals("true") || conditionJs
221                 .equals("false")) {
222             conditionJs = "";
223         }
224 
225         return conditionJs;
226     }
227 
228     /**
229      * Trim, remove expression tokens, and replace common symbols for consistency in parsing and output.
230      *
231      * @param exp the original expression
232      * @return the cleaned up expressiom
233      */
234     private String cleanUpExpression(String exp) {
235         exp = exp.trim();
236         if (exp.startsWith("@{")) {
237             exp = StringUtils.removeStart(exp, "@{");
238             if (exp.endsWith("}")) {
239                 exp = StringUtils.removeEnd(exp, "}");
240             }
241         }
242 
243         // Clean up the expression for parsing consistency
244         exp = StringUtils.replace(exp, "!=", " != ");
245         exp = StringUtils.replace(exp, "==", " == ");
246         exp = StringUtils.replace(exp, ">", " > ");
247         exp = StringUtils.replace(exp, "<", " < ");
248         exp = StringUtils.replace(exp, "<=", " <= ");
249         exp = StringUtils.replace(exp, ">=", " >= ");
250         exp = StringUtils.replace(exp, "&&", " && ");
251         exp = StringUtils.replace(exp, "||", " || ");
252         exp = StringUtils.replace(exp, "  ", " ");
253         exp = StringUtils.replace(exp, " )", ")");
254         exp = StringUtils.replace(exp, "( ", "(");
255         exp = StringUtils.replace(exp, " ,", ",");
256 
257         return exp;
258     }
259 
260     /**
261      * Evaluate server side variables and add them to a map with the key being the original var or call evaluated
262      * to be replaced later by the evaluated value.
263      *
264      * @param exp the expression to evaluate known server variables and methods
265      * @param context the expression evaluation context
266      * @return map of keys that are the original expression/variable, and the replacement value
267      */
268     private Map<String, String> evaluateServerSideVariables(String exp, Map<String, Object> context) {
269         Map<String, String> serverEvaluations = new HashMap<String, String>();
270         Matcher matcher = SERVER_EVALUATION_PATTERN.matcher(exp);
271         while (matcher.find()) {
272             String spelMethodCall = matcher.group(1);
273 
274             Object value = this.evaluateExpression(context, spelMethodCall);
275 
276             // Convert the value to expected js equivalent
277             if (value == null) {
278                 serverEvaluations.put(spelMethodCall, "null");
279             } else if (value instanceof String) {
280                 serverEvaluations.put(spelMethodCall, "\"" + value + "\"");
281             } else if (value instanceof Boolean || NumberUtils.isNumber(value.toString())) {
282                 serverEvaluations.put(spelMethodCall, value.toString());
283             } else {
284                 // Corner case, assume the object gives us something meaningful from toString, wrap in quotes
285                 serverEvaluations.put(spelMethodCall, "\"" + value.toString() + "\"");
286             }
287         }
288 
289         return serverEvaluations;
290     }
291 
292     /**
293      * Replace springEL specific functionality with js equivalents.
294      *
295      * @param conditionJs the original expression
296      * @return the modified expression with js equivalent function calls
297      */
298     private String replaceWithJsEquivalents(String conditionJs) {
299         conditionJs = conditionJs.replaceAll("\\s(?i:ne)\\s", " != ");
300         conditionJs = conditionJs.replaceAll("\\s(?i:eq)\\s", " == ");
301         conditionJs = conditionJs.replaceAll("\\s(?i:gt)\\s", " > ");
302         conditionJs = conditionJs.replaceAll("\\s(?i:lt)\\s", " < ");
303         conditionJs = conditionJs.replaceAll("\\s(?i:lte)\\s", " <= ");
304         conditionJs = conditionJs.replaceAll("\\s(?i:gte)\\s", " >= ");
305         conditionJs = conditionJs.replaceAll("\\s(?i:and)\\s", " && ");
306         conditionJs = conditionJs.replaceAll("\\s(?i:or)\\s", " || ");
307         conditionJs = conditionJs.replaceAll("\\s(?i:not)\\s", " != ");
308         conditionJs = conditionJs.replaceAll("\\s(?i:null)\\s?", " '' ");
309         conditionJs = conditionJs.replaceAll("\\s?(?i:#empty)\\((.*?)\\)", "isValueEmpty($1)");
310         conditionJs = conditionJs.replaceAll("\\s?(?i:#listContains)\\((.*?)\\)", "listContains($1)");
311         conditionJs = conditionJs.replaceAll("\\s?(?i:#emptyList)\\((.*?)\\)", "emptyList($1)");
312 
313         // Handle matches method conversion
314         if (conditionJs.contains("matches")) {
315             conditionJs = conditionJs.replaceAll("\\s+(?i:matches)\\s+'.*'", ".match(/" + "$0" + "/) != null ");
316             conditionJs = conditionJs.replaceAll("\\(/\\s+(?i:matches)\\s+'", "(/");
317             conditionJs = conditionJs.replaceAll("'\\s*/\\)", "/)");
318         }
319 
320         return conditionJs;
321     }
322 
323     /**
324      * {@inheritDoc}
325      */
326     @Override
327     public List<String> findControlNamesInExpression(String exp) {
328         List<String> controlNames = new ArrayList<String>();
329         String stack = "";
330 
331         boolean expectingSingleQuote = false;
332         boolean ignoreNext = false;
333         for (int i = 0; i < exp.length(); i++) {
334             char c = exp.charAt(i);
335             if (!expectingSingleQuote && !ignoreNext && (c == '(' || c == ' ' || c == ')')) {
336                 evaluateCurrentStack(stack.trim(), controlNames);
337                 //reset stack
338                 stack = "";
339                 continue;
340             } else if (!ignoreNext && c == '\'') {
341                 stack = stack + c;
342                 expectingSingleQuote = !expectingSingleQuote;
343             } else if (c == '\\') {
344                 stack = stack + c;
345                 ignoreNext = !ignoreNext;
346             } else {
347                 stack = stack + c;
348                 ignoreNext = false;
349             }
350         }
351 
352         if (StringUtils.isNotEmpty(stack)) {
353             evaluateCurrentStack(stack.trim(), controlNames);
354         }
355 
356         return controlNames;
357     }
358 
359     /**
360      * Used internally by parseExpression to evalute if the current stack is a property
361      * name (ie, will be a control on the form)
362      */
363     protected void evaluateCurrentStack(String stack, List<String> controlNames) {
364         if (StringUtils.isBlank(stack)) {
365             return;
366         }
367 
368         // These are special matches that can be directly replaced to a js equivalent (so skip evaluation of these)
369         if (!(stack.equals("==") || stack.equals("!=") || stack.equals(">") || stack.equals("<") || stack.equals(">=")
370                 || stack.equals("<=") || stack.equalsIgnoreCase("ne") || stack.equalsIgnoreCase("eq") || stack
371                 .equalsIgnoreCase("gt") || stack.equalsIgnoreCase("lt") || stack.equalsIgnoreCase("lte") || stack
372                 .equalsIgnoreCase("gte") || stack.equalsIgnoreCase("matches") || stack.equalsIgnoreCase("null") || stack
373                 .equalsIgnoreCase("false") || stack.equalsIgnoreCase("true") || stack.equalsIgnoreCase("and") || stack
374                 .equalsIgnoreCase("or") || stack.startsWith("#") || stack.equals("!") || stack.startsWith("'") || stack
375                 .endsWith("'"))) {
376 
377             boolean isNumber = NumberUtils.isNumber(stack);
378 
379             // If it is not a number must be check to see if it is a name of a control
380             if (!(isNumber)) {
381                 //correct argument of a custom function ending in comma
382                 if (StringUtils.endsWith(stack, ",")) {
383                     stack = StringUtils.removeEnd(stack, ",").trim();
384                 }
385 
386                 if (!controlNames.contains(stack)) {
387                     controlNames.add(stack);
388                 }
389             }
390         }
391     }
392 
393     /**
394      * {@inheritDoc}
395      */
396     @Override
397     public void initializeEvaluationContext(Object contextObject) {
398         evaluationContext = new StandardEvaluationContext(contextObject);
399 
400         addCustomFunctions(evaluationContext);
401     }
402 
403     /**
404      * {@inheritDoc}
405      */
406     @Override
407     public void evaluateExpressionsOnConfigurable(View view, UifDictionaryBean expressionConfigurable,
408             Map<String, Object> evaluationParameters) {
409         if ((expressionConfigurable instanceof Component) || (expressionConfigurable instanceof LayoutManager)) {
410             evaluatePropertyReplacers(view, expressionConfigurable, evaluationParameters);
411         }
412         evaluatePropertyExpressions(view, expressionConfigurable, evaluationParameters);
413     }
414 
415     /**
416      * {@inheritDoc}
417      */
418     @Override
419     public Object evaluateExpression(Map<String, Object> evaluationParameters, String expressionStr) {
420         Object result = null;
421 
422         // if expression contains placeholders remove before evaluating
423         if (StringUtils.startsWith(expressionStr, UifConstants.EL_PLACEHOLDER_PREFIX) && StringUtils.endsWith(
424                 expressionStr, UifConstants.EL_PLACEHOLDER_SUFFIX)) {
425             expressionStr = StringUtils.removeStart(expressionStr, UifConstants.EL_PLACEHOLDER_PREFIX);
426             expressionStr = StringUtils.removeEnd(expressionStr, UifConstants.EL_PLACEHOLDER_SUFFIX);
427         }
428 
429         try {
430             Expression expression = retrieveCachedExpression(expressionStr);
431 
432             if (evaluationParameters != null) {
433                 evaluationContext.setVariables(evaluationParameters);
434             }
435 
436             result = expression.getValue(evaluationContext);
437         } catch (Exception e) {
438             StringBuilder builder = new StringBuilder("Exception evaluating expression: ");
439             builder.append(expressionStr);
440 
441             if (evaluationParameters != null) {
442                 builder.append(" for the following context: ");
443                 for (Map.Entry<String, Object> entry : evaluationParameters.entrySet()) {
444                     builder.append(System.lineSeparator());
445                     builder.append(entry.getKey());
446                     builder.append(": ");
447                     builder.append(entry.getValue());
448                 }
449             }
450 
451             LOG.error(builder.toString());
452             throw new RuntimeException(builder.toString(), e);
453         }
454 
455         return result;
456     }
457 
458     /**
459      * {@inheritDoc}
460      */
461     @Override
462     public String evaluateExpressionTemplate(Map<String, Object> evaluationParameters, String expressionTemplate) {
463         String result = null;
464 
465         try {
466             Expression expression = retrieveCachedExpression(expressionTemplate);
467 
468             if (evaluationParameters != null) {
469                 evaluationContext.setVariables(evaluationParameters);
470             }
471 
472             result = expression.getValue(evaluationContext, String.class);
473         } catch (Exception e) {
474             LOG.error("Exception evaluating expression: " + expressionTemplate);
475             throw new RuntimeException("Exception evaluating expression: " + expressionTemplate, e);
476         }
477 
478         return result;
479     }
480 
481     /**
482      * {@inheritDoc}
483      */
484     @Override
485     public void evaluatePropertyExpression(View view, Map<String, Object> evaluationParameters,
486             UifDictionaryBean expressionConfigurable, String propertyName, boolean removeExpression) {
487 
488         Map<String, String> propertyExpressions = expressionConfigurable.getPropertyExpressions();
489         if ((propertyExpressions == null) || !propertyExpressions.containsKey(propertyName)) {
490             return;
491         }
492 
493         String expression = propertyExpressions.get(propertyName);
494 
495         // check whether expression should be evaluated or property should retain the expression
496         if (CopyUtils.fieldHasAnnotation(expressionConfigurable.getClass(), propertyName, KeepExpression.class)) {
497             // set expression as property value to be handled by the component
498             ObjectPropertyUtils.setPropertyValue(expressionConfigurable, propertyName, expression);
499             return;
500         }
501 
502         Object propertyValue = null;
503 
504         // replace binding prefixes (lp, dp, fp) in expression before evaluation
505         String adjustedExpression = replaceBindingPrefixes(view, expressionConfigurable, expression);
506 
507         // determine whether the expression is a string template, or evaluates to another object type
508         if (StringUtils.startsWith(adjustedExpression, UifConstants.EL_PLACEHOLDER_PREFIX) && StringUtils.endsWith(
509                 adjustedExpression, UifConstants.EL_PLACEHOLDER_SUFFIX) && (StringUtils.countMatches(adjustedExpression,
510                 UifConstants.EL_PLACEHOLDER_PREFIX) == 1)) {
511             propertyValue = evaluateExpression(evaluationParameters, adjustedExpression);
512         } else {
513             // treat as string template
514             propertyValue = evaluateExpressionTemplate(evaluationParameters, adjustedExpression);
515         }
516 
517         // if property name has the special indicator then we need to add the expression result to the property
518         // value instead of replace
519         if (StringUtils.endsWith(propertyName, ExpressionEvaluator.EMBEDDED_PROPERTY_NAME_ADD_INDICATOR)) {
520             StringUtils.removeEnd(propertyName, ExpressionEvaluator.EMBEDDED_PROPERTY_NAME_ADD_INDICATOR);
521 
522             Collection collectionValue = ObjectPropertyUtils.getPropertyValue(expressionConfigurable, propertyName);
523             if (collectionValue == null) {
524                 throw new RuntimeException("Property name: " + propertyName
525                         + " with collection type was not initialized. Cannot add expression result");
526             }
527             collectionValue.add(propertyValue);
528         } else {
529             ObjectPropertyUtils.setPropertyValue(expressionConfigurable, propertyName, propertyValue);
530         }
531 
532         if (removeExpression) {
533             propertyExpressions.remove(propertyName);
534         }
535     }
536 
537     /**
538      * {@inheritDoc}
539      */
540     @Override
541     public boolean containsElPlaceholder(String value) {
542         boolean containsElPlaceholder = false;
543 
544         if (StringUtils.isNotBlank(value)) {
545             String elPlaceholder = StringUtils.substringBetween(value, UifConstants.EL_PLACEHOLDER_PREFIX,
546                     UifConstants.EL_PLACEHOLDER_SUFFIX);
547             if (StringUtils.isNotBlank(elPlaceholder)) {
548                 containsElPlaceholder = true;
549             }
550         }
551 
552         return containsElPlaceholder;
553     }
554 
555     /**
556      * {@inheritDoc}
557      */
558     @Override
559     public String replaceBindingPrefixes(View view, Object object, String expression) {
560         String adjustedExpression = StringUtils.replace(expression, UifConstants.NO_BIND_ADJUST_PREFIX, "");
561 
562         // replace the field path prefix for DataFields
563         if (StringUtils.contains(adjustedExpression, UifConstants.FIELD_PATH_BIND_ADJUST_PREFIX)) {
564             if (object instanceof DataField) {
565                 // Get the binding path from the object
566                 BindingInfo bindingInfo = ((DataField) object).getBindingInfo();
567 
568                 Pattern pattern = Pattern.compile("(" + Pattern.quote(UifConstants.FIELD_PATH_BIND_ADJUST_PREFIX)
569                         + "[\\.\\w]+" + ")");
570                 Matcher matcher = pattern.matcher(adjustedExpression);
571                 while (matcher.find()) {
572                     String path = matcher.group();
573 
574                     String adjustedPath = bindingInfo.getPropertyAdjustedBindingPath(path);
575                     adjustedExpression = StringUtils.replace(adjustedExpression, path, adjustedPath);
576                 }
577             } else {
578                 adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.FIELD_PATH_BIND_ADJUST_PREFIX,
579                         "");
580             }
581         }
582 
583         // replace the default path prefix if there is one set on the view
584         if (StringUtils.isNotBlank(view.getDefaultBindingObjectPath())) {
585             adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.DEFAULT_PATH_BIND_ADJUST_PREFIX,
586                     view.getDefaultBindingObjectPath() + ".");
587         } else {
588             adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.DEFAULT_PATH_BIND_ADJUST_PREFIX,
589                     "");
590         }
591 
592         // replace line path binding prefix with the actual line path
593         if (adjustedExpression.contains(UifConstants.LINE_PATH_BIND_ADJUST_PREFIX) && (object instanceof Component)) {
594             String linePath = getLinePathPrefixValue((Component) object);
595 
596             adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.LINE_PATH_BIND_ADJUST_PREFIX,
597                     (StringUtils.isEmpty(linePath) ? linePath : linePath + "."));
598         }
599 
600         // replace node path binding prefix with the actual node path
601         if (adjustedExpression.contains(UifConstants.NODE_PATH_BIND_ADJUST_PREFIX) && (object instanceof Component)) {
602             String nodePath = "";
603 
604             Map<String, Object> context = ((Component) object).getContext();
605             if (context != null && context.containsKey(UifConstants.ContextVariableNames.NODE_PATH)) {
606                 nodePath = (String) context.get(UifConstants.ContextVariableNames.NODE_PATH);
607             }
608 
609             adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.NODE_PATH_BIND_ADJUST_PREFIX,
610                     nodePath + ".");
611         }
612 
613         return adjustedExpression;
614     }
615 
616     /**
617      * Attempts to retrieve the {@link Expression} instance for the given expression template, if
618      * not found one is created and added to the cache
619      *
620      * @param expressionTemplate template string for the expression
621      * @return Expression instance
622      */
623     protected Expression retrieveCachedExpression(String expressionTemplate) {
624         Expression expression = null;
625 
626         // return from the expression from cache if present
627         if (cachedExpressions.containsKey(expressionTemplate)) {
628             return cachedExpressions.get(expressionTemplate);
629         }
630 
631         // not in cache, create the expression object
632         if (StringUtils.contains(expressionTemplate, UifConstants.EL_PLACEHOLDER_PREFIX)) {
633             expression = parser.parseExpression(expressionTemplate, new TemplateParserContext(
634                     UifConstants.EL_PLACEHOLDER_PREFIX, UifConstants.EL_PLACEHOLDER_SUFFIX));
635         } else {
636             expression = parser.parseExpression(expressionTemplate);
637         }
638 
639         synchronized (cachedExpressions) {
640             cachedExpressions.put(expressionTemplate, expression);
641         }
642 
643         return expression;
644     }
645 
646     /**
647      * Registers custom functions for el expressions with the given context
648      *
649      * @param context - context instance to register functions to
650      */
651     protected void addCustomFunctions(StandardEvaluationContext context) {
652         context.registerFunction("isAssignableFrom", isAssignableFrom);
653         context.registerFunction("empty", empty);
654         context.registerFunction("emptyList", emptyList);
655         context.registerFunction("getService", getService);
656         context.registerFunction("listContains", listContains);
657         context.registerFunction("getName", getName);
658         context.registerFunction("getParam", getParam);
659         context.registerFunction("getParamAsBoolean", getParamAsBoolean);
660         context.registerFunction("hasPerm", hasPerm);
661         context.registerFunction("hasPermDtls", hasPermDtls);
662         context.registerFunction("hasPermTmpl", hasPermTmpl);
663         context.registerFunction("sequence", sequence);
664         context.registerFunction("getDataObjectKey", getDataObjectKey);
665         context.registerFunction("isProductionEnvironment", isProductionEnvironment);
666     }
667 
668     /**
669      * Iterates through any configured <code>PropertyReplacer</code> instances for the component and
670      * evaluates the given condition. If the condition is met, the replacement value is set on the
671      * corresponding property
672      *
673      * @param view - view instance being rendered
674      * @param expressionConfigurable - expressionConfigurable instance with property replacers list,
675      * should be either a component or layout manager
676      * @param evaluationParameters - parameters for el evaluation
677      */
678     protected void evaluatePropertyReplacers(View view, UifDictionaryBean expressionConfigurable,
679             Map<String, Object> evaluationParameters) {
680         List<PropertyReplacer> replacers = null;
681         if (Component.class.isAssignableFrom(expressionConfigurable.getClass())) {
682             replacers = ((Component) expressionConfigurable).getPropertyReplacers();
683         } else if (LayoutManager.class.isAssignableFrom(expressionConfigurable.getClass())) {
684             replacers = ((LayoutManager) expressionConfigurable).getPropertyReplacers();
685         }
686 
687         if (replacers != null) {
688             for (PropertyReplacer propertyReplacer : replacers) {
689                 String expression = propertyReplacer.getCondition();
690                 String adjustedExpression = replaceBindingPrefixes(view, expressionConfigurable, expression);
691 
692                 String conditionEvaluation = evaluateExpressionTemplate(evaluationParameters, adjustedExpression);
693                 boolean conditionSuccess = Boolean.parseBoolean(conditionEvaluation);
694                 if (conditionSuccess) {
695                     ObjectPropertyUtils.setPropertyValue(expressionConfigurable, propertyReplacer.getPropertyName(),
696                             propertyReplacer.getReplacement());
697                 }
698             }
699         }
700     }
701 
702     /**
703      * Iterates through the keys of the property expressions map and invokes
704      * {@link #evaluatePropertyExpression(org.kuali.rice.krad.uif.view.View, java.util.Map,
705      * org.kuali.rice.krad.datadictionary.uif.UifDictionaryBean, String, boolean)}
706      *
707      * <p>
708      * If the expression is an el template (part static text and part expression), only the
709      * expression part will be replaced with the result. More than one expressions may be contained
710      * within the template
711      * </p>
712      *
713      * @param view - view instance that is being rendered
714      * @param expressionConfigurable - object instance to evaluate expressions for
715      * @param evaluationParameters - map of additional parameters that may be used within the
716      * expressions
717      */
718     protected void evaluatePropertyExpressions(View view, UifDictionaryBean expressionConfigurable,
719             Map<String, Object> evaluationParameters) {
720         if (expressionConfigurable == null) {
721             return;
722         }
723 
724         Map<String, String> propertyExpressions = expressionConfigurable.getPropertyExpressions();
725         if (propertyExpressions == null) {
726             return;
727         }
728 
729         for (String propertyName : propertyExpressions.keySet()) {
730             evaluatePropertyExpression(view, evaluationParameters, expressionConfigurable, propertyName, false);
731         }
732     }
733 
734     /**
735      * Determines the value for the
736      * {@link org.kuali.rice.krad.uif.UifConstants#LINE_PATH_BIND_ADJUST_PREFIX} binding prefix
737      * based on collection group found in the component context
738      *
739      * @param component - component instance for which the prefix is configured on
740      * @return String line binding path or empty string if path not found
741      */
742     protected static String getLinePathPrefixValue(Component component) {
743         Map<String, Object> componentContext = component.getContext();
744         if (componentContext == null) {
745             return "";
746         }
747 
748         CollectionGroup collectionGroup = (CollectionGroup) (componentContext.get(
749                 UifConstants.ContextVariableNames.COLLECTION_GROUP));
750         if (collectionGroup == null) {
751             LOG.warn("collection group not found for " + component + "," + component.getId() + ", " + component
752                     .getComponentTypeName());
753             return "";
754         }
755 
756         String linePath = "";
757 
758         Integer indexObj = (Integer) componentContext.get(UifConstants.ContextVariableNames.INDEX);
759         if (indexObj != null) {
760             int index = indexObj.intValue();
761 
762             boolean addLine = false;
763             Boolean addLineObj = (Boolean) componentContext.get(UifConstants.ContextVariableNames.IS_ADD_LINE);
764 
765             if (addLineObj != null) {
766                 addLine = addLineObj.booleanValue();
767             }
768 
769             if (addLine) {
770                 linePath = collectionGroup.getAddLineBindingInfo().getBindingPath();
771             } else {
772                 linePath = collectionGroup.getBindingInfo().getBindingPath() + "[" + index + "]";
773             }
774         }
775 
776         return linePath;
777     }
778 }