001/**
002 * Copyright 2005-2012 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.service.impl;
017
018import net.sf.ehcache.CacheManager;
019import net.sf.ehcache.Cache;
020import net.sf.ehcache.Element;
021import org.apache.commons.lang.StringUtils;
022import org.kuali.rice.krad.datadictionary.uif.UifDictionaryBean;
023import org.kuali.rice.krad.uif.UifConstants;
024import org.kuali.rice.krad.uif.component.BindingInfo;
025import org.kuali.rice.krad.uif.component.Component;
026import org.kuali.rice.krad.uif.component.KeepExpression;
027import org.kuali.rice.krad.uif.component.PropertyReplacer;
028import org.kuali.rice.krad.uif.container.CollectionGroup;
029import org.kuali.rice.krad.uif.field.DataField;
030import org.kuali.rice.krad.uif.layout.LayoutManager;
031import org.kuali.rice.krad.uif.service.ExpressionEvaluatorService;
032import org.kuali.rice.krad.uif.util.CloneUtils;
033import org.kuali.rice.krad.uif.util.ExpressionFunctions;
034import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
035import org.kuali.rice.krad.uif.view.View;
036import org.springframework.expression.Expression;
037import org.springframework.expression.ExpressionParser;
038import org.springframework.expression.common.TemplateParserContext;
039import org.springframework.expression.spel.standard.SpelExpressionParser;
040import org.springframework.expression.spel.support.StandardEvaluationContext;
041
042import java.lang.reflect.Method;
043import java.util.Collection;
044import java.util.List;
045import java.util.Map;
046import java.util.Map.Entry;
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 ExpressionEvaluatorServiceImpl implements ExpressionEvaluatorService {
054    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(
055            ExpressionEvaluatorServiceImpl.class);
056
057    protected static ExpressionParser parser = new SpelExpressionParser();
058
059    static{
060        CacheManager singletonManager = CacheManager.create();
061        singletonManager.addCache(new Cache("ExpressionEvaluatorServiceCache", 1500, false, false, 120, 0));
062    }
063
064    Cache expressionCache = CacheManager.getInstance().getCache("ExpressionEvaluatorServiceCache");
065
066    private static Method isAssignableFrom;
067    private static Method empty;
068    private static Method emptyList;
069    private static Method listContains;
070    private static Method getName;
071    private static Method getParm;
072    private static Method getParmInd;
073    private static Method hasPerm;
074    private static Method hasPermDtls;
075    private static Method hasPermTmpl;
076    private static Method sequence;
077
078    static{
079        try{
080            isAssignableFrom = ExpressionFunctions.class.getDeclaredMethod("isAssignableFrom", new Class[]{Class.class, Class.class});
081            empty = ExpressionFunctions.class.getDeclaredMethod("empty", new Class[]{Object.class});
082            emptyList = ExpressionFunctions.class.getDeclaredMethod("emptyList", new Class[]{List.class});
083            listContains = ExpressionFunctions.class.getDeclaredMethod("listContains", new Class[]{List.class, Object[].class});
084            getName = ExpressionFunctions.class.getDeclaredMethod("getName", new Class[]{Class.class});
085            getParm = ExpressionFunctions.class.getDeclaredMethod("getParm", new Class[]{String.class, String.class, String.class});
086            getParmInd = ExpressionFunctions.class.getDeclaredMethod("getParmInd", new Class[]{String.class, String.class, String.class});
087            hasPerm = ExpressionFunctions.class.getDeclaredMethod("hasPerm", new Class[]{String.class, String.class});
088            hasPermDtls = ExpressionFunctions.class.getDeclaredMethod("hasPermDtls", new Class[]{String.class, String.class, Map.class, Map.class});
089            hasPermTmpl = ExpressionFunctions.class.getDeclaredMethod("hasPermTmpl", new Class[]{String.class, String.class, Map.class, Map.class});
090            sequence = ExpressionFunctions.class.getDeclaredMethod("sequence", new Class[]{String.class});
091        }catch(NoSuchMethodException e){
092            LOG.error("Custom function for el expressions not found: " + e.getMessage());
093            throw new RuntimeException("Custom function for el expressions not found: " + e.getMessage(), e);
094        }
095    }
096
097    /**
098     * @see org.kuali.rice.krad.uif.service.ExpressionEvaluatorService#evaluateExpressionsOnConfigurable(org.kuali.rice.krad.uif.view.View,
099     *      org.kuali.rice.krad.datadictionary.uif.UifDictionaryBean, Object,
100     *      java.util.Map<String,Object>)
101     */
102    public void evaluateExpressionsOnConfigurable(View view, UifDictionaryBean expressionConfigurable,
103                                                  StandardEvaluationContext context) {
104        if ((expressionConfigurable instanceof Component) || (expressionConfigurable instanceof LayoutManager)) {
105            evaluatePropertyReplacers(view, expressionConfigurable, context);
106        }
107        evaluatePropertyExpressions(view, expressionConfigurable, context);
108    }
109
110    public void evaluateExpressionsOnConfigurable(View view, UifDictionaryBean expressionConfigurable,
111                                                  Object contextObject, Map<String, Object> evaluationParameters) {
112        evaluateExpressionsOnConfigurable(view, expressionConfigurable, getContext(contextObject,evaluationParameters));
113    }
114
115    public String evaluateExpressionTemplate(Object contextObject, Map<String, Object> evaluationParameters,
116                                             String expressionTemplate) {
117        return evaluateExpressionTemplate(getContext(contextObject,evaluationParameters),expressionTemplate);
118    }
119    /**
120     * @see org.kuali.rice.krad.uif.service.ExpressionEvaluatorService#evaluateExpressionTemplate(Object,
121     *      java.util.Map, String)
122     */
123    public String evaluateExpressionTemplate(StandardEvaluationContext context,
124                                             String expressionTemplate) {
125
126        String result = null;
127        try {
128            Expression expression = null;
129            if (StringUtils.contains(expressionTemplate, UifConstants.EL_PLACEHOLDER_PREFIX)) {
130                expression = getTemplateExpression(expressionTemplate);
131            } else {
132                expression = getExpression(expressionTemplate);
133            }
134
135            result = expression.getValue(context, String.class);
136        } catch (Exception e) {
137            LOG.error("Exception evaluating expression: " + expressionTemplate);
138            throw new RuntimeException("Exception evaluating expression: " + expressionTemplate, e);
139        }
140
141        return result;
142    }
143
144    public Object evaluateExpression(Object contextObject, Map<String, Object> evaluationParameters,
145                                     String expressionStr) {
146        return evaluateExpression(getContext(contextObject,evaluationParameters),expressionStr);
147    }
148
149    /**
150     * @see org.kuali.rice.krad.uif.service.ExpressionEvaluatorService#evaluateExpression(Object,
151     *      java.util.Map, String)
152     */
153    public Object evaluateExpression(StandardEvaluationContext context,
154            String expressionStr) {
155
156        // if expression contains placeholders remove before evaluating
157        if (StringUtils.startsWith(expressionStr, UifConstants.EL_PLACEHOLDER_PREFIX) && StringUtils.endsWith(
158                expressionStr, UifConstants.EL_PLACEHOLDER_SUFFIX)) {
159            expressionStr = StringUtils.removeStart(expressionStr, UifConstants.EL_PLACEHOLDER_PREFIX);
160            expressionStr = StringUtils.removeEnd(expressionStr, UifConstants.EL_PLACEHOLDER_SUFFIX);
161        }
162
163
164        Object result = null;
165        try {
166            Expression expression = getExpression(expressionStr);
167
168            result = expression.getValue(context);
169        } catch (Exception e) {
170            LOG.error("Exception evaluating expression: " + expressionStr);
171            throw new RuntimeException("Exception evaluating expression: " + expressionStr, e);
172        }
173
174        return result;
175    }
176
177    public StandardEvaluationContext getContext(Object contextObject, Map<String, Object> evaluationParameters){
178        StandardEvaluationContext context = new StandardEvaluationContext(contextObject);
179        context.setVariables(evaluationParameters);
180        addCustomFunctions(context);
181        return context;
182    }
183
184    private Expression getTemplateExpression(String expressionTemplate) {
185        Element cachedResult = expressionCache.get(expressionTemplate);
186        Object result;
187        if(cachedResult==null){
188            result = parser.parseExpression(expressionTemplate, new TemplateParserContext(
189                    UifConstants.EL_PLACEHOLDER_PREFIX, UifConstants.EL_PLACEHOLDER_SUFFIX));;
190            expressionCache.put(new Element(expressionTemplate, result));
191        }else{
192            result = cachedResult.getObjectValue();
193        }
194        return (Expression)result;
195
196    }
197
198    private Expression getExpression(String expressionStr) {
199        Element cachedResult = expressionCache.get(expressionStr);
200        Object result;
201        if(cachedResult==null){
202            result = parser.parseExpression(expressionStr);
203            expressionCache.put(new Element(expressionStr,result));
204        }else{
205            result = cachedResult.getObjectValue();
206        }
207        return (Expression)result;
208    }
209
210    /**
211     * Registers custom functions for el expressions with the given context
212     *
213     * @param context - context instance to register functions to
214     */
215    protected void addCustomFunctions(StandardEvaluationContext context) {
216            // TODO: possibly reflect ExpressionFunctions and add automatically
217        context.registerFunction("isAssignableFrom", isAssignableFrom);
218        context.registerFunction("empty", empty);
219        context.registerFunction("emptyList", emptyList);
220        context.registerFunction("listContains", listContains);
221        context.registerFunction("getName", getName);
222        context.registerFunction("getParm", getParm);
223        context.registerFunction("getParmInd", getParmInd);
224        context.registerFunction("hasPerm", hasPerm);
225        context.registerFunction("hasPermDtls", hasPermDtls);
226        context.registerFunction("hasPermTmpl", hasPermTmpl);
227        context.registerFunction("sequence", sequence);
228
229    }
230
231    /**
232     * Iterates through any configured <code>PropertyReplacer</code> instances for the component and
233     * evaluates the given condition. If the condition is met, the replacement value is set on the
234     * corresponding property
235     *
236     * @param view - view instance being rendered
237     * @param expressionConfigurable - expressionConfigurable instance with property replacers list, should be either a
238     * component or layout
239     * manager
240     * @param context - context for el evaluation
241     */
242    protected void evaluatePropertyReplacers(View view, UifDictionaryBean expressionConfigurable,
243                                             StandardEvaluationContext context) {
244        List<PropertyReplacer> replacers = null;
245        if (Component.class.isAssignableFrom(expressionConfigurable.getClass())) {
246            replacers = ((Component) expressionConfigurable).getPropertyReplacers();
247        } else if (LayoutManager.class.isAssignableFrom(expressionConfigurable.getClass())) {
248            replacers = ((LayoutManager) expressionConfigurable).getPropertyReplacers();
249        }
250
251        for (PropertyReplacer propertyReplacer : replacers) {
252            String expression = propertyReplacer.getCondition();
253            String adjustedExpression = replaceBindingPrefixes(view, expressionConfigurable, expression);
254
255            String conditionEvaluation = evaluateExpressionTemplate(context,
256                    adjustedExpression);
257            boolean conditionSuccess = Boolean.parseBoolean(conditionEvaluation);
258            if (conditionSuccess) {
259                ObjectPropertyUtils.setPropertyValue(expressionConfigurable, propertyReplacer.getPropertyName(),
260                        propertyReplacer.getReplacement());
261            }
262        }
263    }
264    protected void evaluatePropertyReplacers(View view, UifDictionaryBean expressionConfigurable, Object contextObject,
265                                             Map<String, Object> evaluationParameters) {
266        evaluatePropertyReplacers(view,expressionConfigurable,getContext(contextObject,evaluationParameters));
267    }
268
269    /**
270     * Retrieves the Map from the given object that containing the property expressions that should
271     * be evaluated. Each expression is then evaluated and the result is used to set the property value
272     *
273     * <p>
274     * If the expression is an el template (part static text and part expression), only the expression
275     * part will be replaced with the result. More than one expressions may be contained within the template
276     * </p>
277     *
278     * @param view - view instance that is being rendered
279     * @param expressionConfigurable - object instance to evaluate expressions for
280     * @param context - object providing the default context for expressions
281     */
282    protected void evaluatePropertyExpressions(View view, UifDictionaryBean expressionConfigurable,
283                                               StandardEvaluationContext context) {
284        Map<String, String> propertyExpressions = expressionConfigurable.getPropertyExpressions();
285        for (Entry<String, String> propertyExpression : propertyExpressions.entrySet()) {
286            String propertyName = propertyExpression.getKey();
287            String expression = propertyExpression.getValue();
288
289            // check whether expression should be evaluated or property should retain the expression
290            if (CloneUtils.fieldHasAnnotation(expressionConfigurable.getClass(), propertyName, KeepExpression.class)) {
291                // set expression as property value to be handled by the component
292                ObjectPropertyUtils.setPropertyValue(expressionConfigurable, propertyName, expression);
293                continue;
294            }
295
296            Object propertyValue = null;
297
298            // replace binding prefixes (lp, dp, fp) in expression before evaluation
299            String adjustedExpression = replaceBindingPrefixes(view, expressionConfigurable, expression);
300
301            // determine whether the expression is a string template, or evaluates to another object type
302            if (StringUtils.startsWith(adjustedExpression, UifConstants.EL_PLACEHOLDER_PREFIX) && StringUtils.endsWith(
303                    adjustedExpression, UifConstants.EL_PLACEHOLDER_SUFFIX) && (StringUtils.countMatches(
304                    adjustedExpression, UifConstants.EL_PLACEHOLDER_PREFIX) == 1)) {
305                propertyValue = evaluateExpression(context, adjustedExpression);
306            } else {
307                // treat as string template
308                propertyValue = evaluateExpressionTemplate(context, adjustedExpression);
309            }
310
311            // if property name has the special indicator then we need to add the expression result to the property
312            // value instead of replace
313            if (StringUtils.endsWith(propertyName, ExpressionEvaluatorService.EMBEDDED_PROPERTY_NAME_ADD_INDICATOR)) {
314                StringUtils.removeEnd(propertyName, ExpressionEvaluatorService.EMBEDDED_PROPERTY_NAME_ADD_INDICATOR);
315
316                Collection collectionValue = ObjectPropertyUtils.getPropertyValue(expressionConfigurable, propertyName);
317                if (collectionValue == null) {
318                    throw new RuntimeException("Property name: "
319                            + propertyName
320                            + " with collection type was not initialized. Cannot add expression result");
321                }
322                collectionValue.add(propertyValue);
323            } else {
324                ObjectPropertyUtils.setPropertyValue(expressionConfigurable, propertyName, propertyValue);
325            }
326        }
327    }
328
329    protected void evaluatePropertyExpressions(View view, UifDictionaryBean expressionConfigurable,
330                                               Object contextObject, Map<String, Object> evaluationParameters) {
331        evaluatePropertyExpressions(view,expressionConfigurable,getContext(contextObject,evaluationParameters));
332    }
333
334    /**
335     * @see org.kuali.rice.krad.uif.service.ExpressionEvaluatorService#containsElPlaceholder(String)
336     */
337    public boolean containsElPlaceholder(String value) {
338        boolean containsElPlaceholder = false;
339
340        if (StringUtils.isNotBlank(value)) {
341            String elPlaceholder = StringUtils.substringBetween(value, UifConstants.EL_PLACEHOLDER_PREFIX,
342                    UifConstants.EL_PLACEHOLDER_SUFFIX);
343            if (StringUtils.isNotBlank(elPlaceholder)) {
344                containsElPlaceholder = true;
345            }
346        }
347
348        return containsElPlaceholder;
349    }
350
351    /**
352     * @see org.kuali.rice.krad.uif.service.ExpressionEvaluatorService#replaceBindingPrefixes(org.kuali.rice.krad.uif.view.View,
353     *      Object, String)
354     */
355    public String replaceBindingPrefixes(View view, Object object, String expression) {
356        String adjustedExpression = StringUtils.replace(expression, UifConstants.NO_BIND_ADJUST_PREFIX, "");
357
358        // replace the field path prefix for DataFields
359        if (object instanceof DataField) {
360
361            // Get the binding path from the object
362            BindingInfo bindingInfo = ((DataField) object).getBindingInfo();
363            String fieldPath = bindingInfo.getBindingPath();
364
365            // Remove the property name from the binding path
366            fieldPath = StringUtils.removeEnd(fieldPath, "." + bindingInfo.getBindingName());
367            adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.FIELD_PATH_BIND_ADJUST_PREFIX,
368                    fieldPath + ".");
369        } else {
370            adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.FIELD_PATH_BIND_ADJUST_PREFIX,
371                    "");
372        }
373
374        // replace the default path prefix if there is one set on the view
375        if (StringUtils.isNotBlank(view.getDefaultBindingObjectPath())) {
376            adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.DEFAULT_PATH_BIND_ADJUST_PREFIX,
377                    view.getDefaultBindingObjectPath() + ".");
378
379        } else {
380            adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.DEFAULT_PATH_BIND_ADJUST_PREFIX,
381                    "");
382        }
383
384        // replace line path binding prefix with the actual line path
385        if (adjustedExpression.contains(UifConstants.LINE_PATH_BIND_ADJUST_PREFIX) && (object instanceof Component)) {
386            String linePath = getLinePathPrefixValue((Component) object);
387
388            adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.LINE_PATH_BIND_ADJUST_PREFIX,
389                    linePath + ".");
390        }
391
392        // replace node path binding prefix with the actual node path
393        if (adjustedExpression.contains(UifConstants.NODE_PATH_BIND_ADJUST_PREFIX) && (object instanceof Component)) {
394            String nodePath = "";
395
396            Map<String, Object> context = ((Component) object).getContext();
397            if (context.containsKey(UifConstants.ContextVariableNames.NODE_PATH)) {
398                nodePath = (String) context.get(UifConstants.ContextVariableNames.NODE_PATH);
399            }
400
401            adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.NODE_PATH_BIND_ADJUST_PREFIX,
402                    nodePath + ".");
403        }
404
405        return adjustedExpression;
406    }
407
408    /**
409     * Determines the value for the {@link org.kuali.rice.krad.uif.UifConstants#LINE_PATH_BIND_ADJUST_PREFIX} binding
410     * prefix
411     * based on collection group found in the component context
412     *
413     * @param component - component instance for which the prefix is configured on
414     * @return String line binding path or empty string if path not found
415     */
416    protected static String getLinePathPrefixValue(Component component) {
417        String linePath = "";
418
419        CollectionGroup collectionGroup = (CollectionGroup) (component.getContext().get(
420                UifConstants.ContextVariableNames.COLLECTION_GROUP));
421        if (collectionGroup == null) {
422            LOG.warn("collection group not found for " + component + "," + component.getId() + ", " + component
423                    .getComponentTypeName());
424            return linePath;
425        }
426
427        Object indexObj = component.getContext().get(UifConstants.ContextVariableNames.INDEX);
428        if (indexObj != null) {
429            int index = (Integer) indexObj;
430            boolean addLine = false;
431            Object addLineObj = component.getContext().get(UifConstants.ContextVariableNames.IS_ADD_LINE);
432
433            if (addLineObj != null) {
434                addLine = (Boolean) addLineObj;
435            }
436
437            if (addLine) {
438                linePath = collectionGroup.getAddLineBindingInfo().getBindingPath();
439            } else {
440                linePath = collectionGroup.getBindingInfo().getBindingPath() + "[" + index + "]";
441            }
442        }
443
444        return linePath;
445    }
446
447}