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