001/**
002 * Copyright 2005-2016 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.util;
017
018import org.apache.commons.lang.StringUtils;
019import org.apache.commons.logging.Log;
020import org.apache.commons.logging.LogFactory;
021import org.kuali.rice.krad.uif.UifConstants;
022import org.kuali.rice.krad.uif.UifPropertyPaths;
023import org.kuali.rice.krad.uif.component.Configurable;
024import org.springframework.beans.BeansException;
025import org.springframework.beans.MutablePropertyValues;
026import org.springframework.beans.PropertyValue;
027import org.springframework.beans.factory.config.BeanDefinition;
028import org.springframework.beans.factory.config.BeanDefinitionHolder;
029import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
030import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
031import org.springframework.beans.factory.config.TypedStringValue;
032import org.springframework.beans.factory.support.BeanDefinitionRegistry;
033import org.springframework.beans.factory.support.GenericBeanDefinition;
034import org.springframework.beans.factory.support.ManagedList;
035import org.springframework.beans.factory.support.ManagedMap;
036
037import java.util.ArrayList;
038import java.util.HashMap;
039import java.util.HashSet;
040import java.util.LinkedHashMap;
041import java.util.LinkedHashSet;
042import java.util.List;
043import java.util.Map;
044import java.util.Set;
045
046/**
047 * Post processes the bean factory to handle UIF property expressions and IDs on inner beans
048 *
049 * <p>
050 * Conditional logic can be implemented with the UIF dictionary by means of property expressions. These are
051 * expressions that follow SPEL and can be given as the value for a property using the @{} placeholder. Since such
052 * a value would cause an exception when creating the object if the property is a non-string type (value cannot be
053 * converted), we need to move those expressions to a Map for processing, and then remove the original property
054 * configuration containing the expression. The expressions are then evaluated during the view apply model phase and
055 * the result is set as the value for the corresponding property.
056 * </p>
057 *
058 * <p>
059 * Spring will not register inner beans with IDs so that the bean definition can be retrieved through the factory,
060 * therefore this post processor adds them as top level registered beans
061 * </p>
062 *
063 * @author Kuali Rice Team (rice.collab@kuali.org)
064 */
065public class UifBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
066    private static final Log LOG = LogFactory.getLog(UifBeanFactoryPostProcessor.class);
067
068    /**
069     * Iterates through all beans in the factory and invokes processing
070     *
071     * @param beanFactory - bean factory instance to process
072     * @throws BeansException
073     */
074    @Override
075    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
076        Set<String> processedBeanNames = new HashSet<String>();
077
078        LOG.info("Beginning post processing of bean factory for UIF expressions");
079
080        String[] beanNames = beanFactory.getBeanDefinitionNames();
081        for (int i = 0; i < beanNames.length; i++) {
082            String beanName = beanNames[i];
083            BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
084
085            processBeanDefinition(beanName, beanDefinition, beanFactory, processedBeanNames);
086        }
087
088        LOG.info("Finished post processing of bean factory for UIF expressions");
089    }
090
091    /**
092     * If the bean class is type Component, LayoutManager, or BindingInfo, iterate through configured property values
093     * and check for expressions
094     *
095     * <p>
096     * If a expression is found for a property, it is added to the 'propertyExpressions' map and then the original
097     * property value is removed to prevent binding errors (when converting to a non string type)
098     * </p>
099     *
100     * @param beanName - name of the bean in the factory (only set for top level beans, not nested)
101     * @param beanDefinition - bean definition to process for expressions
102     * @param beanFactory - bean factory being processed
103     */
104    protected void processBeanDefinition(String beanName, BeanDefinition beanDefinition,
105            ConfigurableListableBeanFactory beanFactory, Set<String> processedBeanNames) {
106        Class<?> beanClass = getBeanClass(beanDefinition, beanFactory);
107        if ((beanClass == null) || !Configurable.class.isAssignableFrom(beanClass)) {
108            return;
109        }
110
111        if (processedBeanNames.contains(beanName)) {
112            return;
113        }
114
115        LOG.debug("Processing bean name '" + beanName + "'");
116
117        MutablePropertyValues pvs = beanDefinition.getPropertyValues();
118
119        if (pvs.getPropertyValue(UifPropertyPaths.PROPERTY_EXPRESSIONS) != null) {
120            // already processed so skip (could be reloading dictionary)
121            return;
122        }
123
124        Map<String, String> propertyExpressions = new ManagedMap<String, String>();
125        Map<String, String> parentPropertyExpressions = getPropertyExpressionsFromParent(beanDefinition.getParentName(),
126                beanFactory, processedBeanNames);
127        boolean parentExpressionsExist = !parentPropertyExpressions.isEmpty();
128
129        // process expressions on property values
130        PropertyValue[] pvArray = pvs.getPropertyValues();
131        for (PropertyValue pv : pvArray) {
132            if (hasExpression(pv.getValue())) {
133                // process expression
134                String strValue = getStringValue(pv.getValue());
135                propertyExpressions.put(pv.getName(), strValue);
136
137                // remove property value so expression will not cause binding exception
138                pvs.removePropertyValue(pv.getName());
139            } else {
140                // process nested objects
141                Object newValue = processPropertyValue(pv.getName(), pv.getValue(), parentPropertyExpressions,
142                        propertyExpressions, beanFactory, processedBeanNames);
143                pvs.removePropertyValue(pv.getName());
144                pvs.addPropertyValue(pv.getName(), newValue);
145            }
146
147            // removed expression (if exists) from parent map since the property was set on child
148            if (parentPropertyExpressions.containsKey(pv.getName())) {
149                parentPropertyExpressions.remove(pv.getName());
150            }
151
152            // if property is nested, need to override any parent expressions set on nested beans
153            if (StringUtils.contains(pv.getName(), ".")) {
154                //removeParentExpressionsOnNested(pv.getName(), pvs, beanDefinition.getParentName(), beanFactory);
155            }
156        }
157
158        if (!propertyExpressions.isEmpty() || parentExpressionsExist) {
159            // merge two maps
160            ManagedMap<String, String> mergedPropertyExpressions = new ManagedMap<String, String>();
161            mergedPropertyExpressions.setMergeEnabled(false);
162            mergedPropertyExpressions.putAll(parentPropertyExpressions);
163            mergedPropertyExpressions.putAll(propertyExpressions);
164
165            pvs.addPropertyValue(UifPropertyPaths.PROPERTY_EXPRESSIONS, mergedPropertyExpressions);
166        }
167
168        // if bean name is given and factory does not have it registered we need to add it (inner beans that
169        // were given an id)
170        if (StringUtils.isNotBlank(beanName) && !StringUtils.contains(beanName, "$") && !StringUtils.contains(beanName,
171                "#") && !beanFactory.containsBean(beanName)) {
172            ((BeanDefinitionRegistry) beanFactory).registerBeanDefinition(beanName, beanDefinition);
173        }
174
175        if (StringUtils.isNotBlank(beanName)) {
176            processedBeanNames.add(beanName);
177        }
178    }
179
180    protected void removeParentExpressionsOnNested(String propertyName, MutablePropertyValues pvs,
181            String parentBeanName, ConfigurableListableBeanFactory beanFactory) {
182        BeanDefinition parentBeanDefinition = beanFactory.getMergedBeanDefinition(parentBeanName);
183
184        // TODO: this only handles one level of nesting
185        MutablePropertyValues parentPvs = parentBeanDefinition.getPropertyValues();
186        PropertyValue[] pvArray = parentPvs.getPropertyValues();
187        for (PropertyValue pv : pvArray) {
188            boolean isNameMatch = false;
189            String nestedPropertyName = "";
190            if (propertyName.startsWith(pv.getName())) {
191                nestedPropertyName = StringUtils.removeStart(propertyName, pv.getName());
192                if (nestedPropertyName.startsWith(".")) {
193                    nestedPropertyName = StringUtils.removeStart(nestedPropertyName, ".");
194                    isNameMatch = true;
195                }
196            }
197
198            // if property name from parent matches and is a bean definition, check for property expressions map
199            if (isNameMatch && ((pv.getValue() instanceof BeanDefinition) || (pv
200                    .getValue() instanceof BeanDefinitionHolder))) {
201                BeanDefinition propertyBeanDefinition;
202                if (pv.getValue() instanceof BeanDefinition) {
203                    propertyBeanDefinition = (BeanDefinition) pv.getValue();
204                } else {
205                    propertyBeanDefinition = ((BeanDefinitionHolder) pv.getValue()).getBeanDefinition();
206                }
207
208                MutablePropertyValues nestedPvs = propertyBeanDefinition.getPropertyValues();
209                if (nestedPvs.contains(UifPropertyPaths.PROPERTY_EXPRESSIONS)) {
210                    PropertyValue propertyExpressionsPV = nestedPvs.getPropertyValue(
211                            UifPropertyPaths.PROPERTY_EXPRESSIONS);
212                    if (propertyExpressionsPV != null) {
213                        Object value = propertyExpressionsPV.getValue();
214                        if ((value != null) && (value instanceof ManagedMap)) {
215                            Map<String, String> nestedPropertyExpressions = (ManagedMap) value;
216                            if (nestedPropertyExpressions.containsKey(nestedPropertyName)) {
217                                // need to make copy of property value with expression removed from map
218                                ManagedMap<String, String> copiedPropertyExpressions = new ManagedMap<String, String>();
219                                copiedPropertyExpressions.setMergeEnabled(false);
220                                copiedPropertyExpressions.putAll(nestedPropertyExpressions);
221                                copiedPropertyExpressions.remove(nestedPropertyName);
222
223                                BeanDefinition copiedBeanDefinition = new GenericBeanDefinition(propertyBeanDefinition);
224                                copiedBeanDefinition.getPropertyValues().add(UifPropertyPaths.PROPERTY_EXPRESSIONS,
225                                        copiedPropertyExpressions);
226
227                                pvs.add(pv.getName(), copiedBeanDefinition);
228                            }
229                        }
230                    }
231                }
232            }
233        }
234    }
235
236    /**
237     * Retrieves the class for the object that will be created from the bean definition. Since the class might not
238     * be configured on the bean definition, but by a parent, each parent bean definition is recursively checked for
239     * a class until one is found
240     *
241     * @param beanDefinition - bean definition to get class for
242     * @param beanFactory - bean factory that contains the bean definition
243     * @return Class<?> class configured for the bean definition, or null
244     */
245    protected Class<?> getBeanClass(BeanDefinition beanDefinition, ConfigurableListableBeanFactory beanFactory) {
246        if (StringUtils.isNotBlank(beanDefinition.getBeanClassName())) {
247            try {
248                return Class.forName(beanDefinition.getBeanClassName());
249            } catch (ClassNotFoundException e) {
250                // swallow exception and return null so bean is not processed
251                return null;
252            }
253        } else if (StringUtils.isNotBlank(beanDefinition.getParentName())) {
254            BeanDefinition parentBeanDefinition = beanFactory.getBeanDefinition(beanDefinition.getParentName());
255            if (parentBeanDefinition != null) {
256                return getBeanClass(parentBeanDefinition, beanFactory);
257            }
258        }
259
260        return null;
261    }
262
263    /**
264     * Retrieves the property expressions map set on the bean with given name. If the bean has not been processed
265     * by the bean factory post processor, that is done before retrieving the map
266     *
267     * @param parentBeanName - name of the parent bean to retrieve map for (if empty a new map will be returned)
268     * @param beanFactory - bean factory to retrieve bean definition from
269     * @param processedBeanNames - set of bean names that have been processed so far
270     * @return Map<String, String> property expressions map from parent or new instance
271     */
272    protected Map<String, String> getPropertyExpressionsFromParent(String parentBeanName,
273            ConfigurableListableBeanFactory beanFactory, Set<String> processedBeanNames) {
274        Map<String, String> propertyExpressions = new HashMap<String, String>();
275        if (StringUtils.isBlank(parentBeanName) || !beanFactory.containsBeanDefinition(parentBeanName)) {
276            return propertyExpressions;
277        }
278
279        if (!processedBeanNames.contains(parentBeanName)) {
280            processBeanDefinition(parentBeanName, beanFactory.getBeanDefinition(parentBeanName), beanFactory,
281                    processedBeanNames);
282        }
283
284        BeanDefinition beanDefinition = beanFactory.getBeanDefinition(parentBeanName);
285        MutablePropertyValues pvs = beanDefinition.getPropertyValues();
286
287        PropertyValue propertyExpressionsPV = pvs.getPropertyValue(UifPropertyPaths.PROPERTY_EXPRESSIONS);
288        if (propertyExpressionsPV != null) {
289            Object value = propertyExpressionsPV.getValue();
290            if ((value != null) && (value instanceof ManagedMap)) {
291                propertyExpressions.putAll((ManagedMap) value);
292            }
293        }
294
295        return propertyExpressions;
296    }
297
298    /**
299     * Checks whether the given property value is of String type, and if so whether it contains the expression
300     * placholder(s)
301     *
302     * @param propertyValue - value to check for expressions
303     * @return boolean true if the property value contains expression(s), false if it does not
304     */
305    protected boolean hasExpression(Object propertyValue) {
306        if (propertyValue != null) {
307            // if value is string, check for el expression
308            String strValue = getStringValue(propertyValue);
309            if (strValue != null) {
310                String elPlaceholder = StringUtils.substringBetween(strValue, UifConstants.EL_PLACEHOLDER_PREFIX,
311                        UifConstants.EL_PLACEHOLDER_SUFFIX);
312                if (StringUtils.isNotBlank(elPlaceholder)) {
313                    return true;
314                }
315            }
316        }
317
318        return false;
319    }
320
321    /**
322     * Processes the given property name/value pair for complex objects, such as bean definitions or collections,
323     * which if found will be processed for contained property expression values
324     *
325     * @param propertyName - name of the property whose value is being processed
326     * @param propertyValue - value to check
327     * @param parentPropertyExpressions - map that holds property expressions for the parent bean definition, used for
328     * merging
329     * @param propertyExpressions - map that holds property expressions for the bean definition being processed
330     * @param beanFactory - bean factory that contains the bean definition being processed
331     * @param processedBeanNames - set of bean names that have been processed so far
332     * @return Object new value to set for property
333     */
334    protected Object processPropertyValue(String propertyName, Object propertyValue,
335            Map<String, String> parentPropertyExpressions, Map<String, String> propertyExpressions,
336            ConfigurableListableBeanFactory beanFactory, Set<String> processedBeanNames) {
337        if (propertyValue == null) {
338            return null;
339        }
340
341        // process nested bean definitions
342        if ((propertyValue instanceof BeanDefinition) || (propertyValue instanceof BeanDefinitionHolder)) {
343            String beanName = null;
344            BeanDefinition beanDefinition;
345            if (propertyValue instanceof BeanDefinition) {
346                beanDefinition = (BeanDefinition) propertyValue;
347            } else {
348                beanDefinition = ((BeanDefinitionHolder) propertyValue).getBeanDefinition();
349                beanName = ((BeanDefinitionHolder) propertyValue).getBeanName();
350            }
351
352            // since overriding the entire bean, clear any expressions from parent that start with the bean property
353            removeExpressionsByPrefix(propertyName, parentPropertyExpressions);
354            processBeanDefinition(beanName, beanDefinition, beanFactory, processedBeanNames);
355
356            return propertyValue;
357        }
358
359        // recurse into collections
360        if (propertyValue instanceof Object[]) {
361            visitArray(propertyName, parentPropertyExpressions, propertyExpressions, (Object[]) propertyValue,
362                    beanFactory, processedBeanNames);
363        } else if (propertyValue instanceof List) {
364            visitList(propertyName, parentPropertyExpressions, propertyExpressions, (List) propertyValue, beanFactory,
365                    processedBeanNames);
366        } else if (propertyValue instanceof Set) {
367            visitSet(propertyName, parentPropertyExpressions, propertyExpressions, (Set) propertyValue, beanFactory,
368                    processedBeanNames);
369        } else if (propertyValue instanceof Map) {
370            visitMap(propertyName, parentPropertyExpressions, propertyExpressions, (Map) propertyValue, beanFactory,
371                    processedBeanNames);
372        }
373
374        // others (primitive) just return value as is
375        return propertyValue;
376    }
377
378    /**
379     * Removes entries from the given expressions map whose key starts with the given prefix
380     *
381     * @param propertyNamePrefix - prefix to search for and remove
382     * @param propertyExpressions - map of property expressions to filter
383     */
384    protected void removeExpressionsByPrefix(String propertyNamePrefix, Map<String, String> propertyExpressions) {
385        Map<String, String> adjustedPropertyExpressions = new HashMap<String, String>();
386        for (String propertyName : propertyExpressions.keySet()) {
387            if (!propertyName.startsWith(propertyNamePrefix)) {
388                adjustedPropertyExpressions.put(propertyName, propertyExpressions.get(propertyName));
389            }
390        }
391
392        propertyExpressions.clear();
393        propertyExpressions.putAll(adjustedPropertyExpressions);
394    }
395
396    /**
397     * Determines whether the given value is of String type and if so returns the string value
398     *
399     * @param value - object value to check
400     * @return String string value for object or null if object is not a string type
401     */
402    protected String getStringValue(Object value) {
403        if (value instanceof TypedStringValue) {
404            TypedStringValue typedStringValue = (TypedStringValue) value;
405            return typedStringValue.getValue();
406        } else if (value instanceof String) {
407            return (String) value;
408        }
409
410        return null;
411    }
412
413    @SuppressWarnings("unchecked")
414    protected void visitArray(String propertyName, Map<String, String> parentPropertyExpressions,
415            Map<String, String> propertyExpressions, Object[] arrayVal, ConfigurableListableBeanFactory beanFactory,
416            Set<String> processedBeanNames) {
417        for (int i = 0; i < arrayVal.length; i++) {
418            Object elem = arrayVal[i];
419            String elemPropertyName = propertyName + "[" + i + "]";
420
421            if (hasExpression(elem)) {
422                String strValue = getStringValue(elem);
423                propertyExpressions.put(elemPropertyName, strValue);
424                arrayVal[i] = null;
425            } else {
426                Object newElem = processPropertyValue(elemPropertyName, elem, parentPropertyExpressions,
427                        propertyExpressions, beanFactory, processedBeanNames);
428                arrayVal[i] = newElem;
429            }
430
431            if (parentPropertyExpressions.containsKey(elemPropertyName)) {
432                parentPropertyExpressions.remove(elemPropertyName);
433            }
434        }
435    }
436
437    @SuppressWarnings("unchecked")
438    protected void visitList(String propertyName, Map<String, String> parentPropertyExpressions,
439            Map<String, String> propertyExpressions, List listVal, ConfigurableListableBeanFactory beanFactory,
440            Set<String> processedBeanNames) {
441        List newList = new ArrayList();
442
443        for (int i = 0; i < listVal.size(); i++) {
444            Object elem = listVal.get(i);
445            String elemPropertyName = propertyName + "[" + i + "]";
446
447            if (hasExpression(elem)) {
448                String strValue = getStringValue(elem);
449                propertyExpressions.put(elemPropertyName, strValue);
450                newList.add(i, null);
451            } else {
452                Object newElem = processPropertyValue(elemPropertyName, elem, parentPropertyExpressions,
453                        propertyExpressions, beanFactory, processedBeanNames);
454                newList.add(i, newElem);
455            }
456
457            if (parentPropertyExpressions.containsKey(elemPropertyName)) {
458                parentPropertyExpressions.remove(elemPropertyName);
459            }
460        }
461
462        // determine if we need to clear any parent expressions for this list
463        if (listVal instanceof ManagedList) {
464            boolean isMergeEnabled = ((ManagedList) listVal).isMergeEnabled();
465            if (!isMergeEnabled) {
466                // clear any expressions that match the property name minus index
467                Map<String, String> adjustedParentExpressions = new HashMap<String, String>();
468                for (Map.Entry<String, String> parentExpression : parentPropertyExpressions.entrySet()) {
469                    if (!parentExpression.getKey().startsWith(propertyName + "[")) {
470                        adjustedParentExpressions.put(parentExpression.getKey(), parentExpression.getValue());
471                    }
472                }
473
474                parentPropertyExpressions.clear();
475                parentPropertyExpressions.putAll(adjustedParentExpressions);
476            }
477        }
478
479        listVal.clear();
480        listVal.addAll(newList);
481    }
482
483    @SuppressWarnings("unchecked")
484    protected void visitSet(String propertyName, Map<String, String> parentPropertyExpressions,
485            Map<String, String> propertyExpressions, Set setVal, ConfigurableListableBeanFactory beanFactory,
486            Set<String> processedBeanNames) {
487        Set newContent = new LinkedHashSet();
488
489        // TODO: this is not handled correctly
490        for (Object elem : setVal) {
491            Object newElem = processPropertyValue(propertyName, elem, parentPropertyExpressions, propertyExpressions,
492                    beanFactory, processedBeanNames);
493            newContent.add(newElem);
494        }
495
496        setVal.clear();
497        setVal.addAll(newContent);
498    }
499
500    @SuppressWarnings("unchecked")
501    protected void visitMap(String propertyName, Map<String, String> parentPropertyExpressions,
502            Map<String, String> propertyExpressions, Map<?, ?> mapVal, ConfigurableListableBeanFactory beanFactory,
503            Set<String> processedBeanNames) {
504        Map newContent = new LinkedHashMap();
505
506        boolean isMergeEnabled = false;
507        if (mapVal instanceof ManagedMap) {
508            isMergeEnabled = ((ManagedMap) mapVal).isMergeEnabled();
509        }
510
511        for (Map.Entry entry : mapVal.entrySet()) {
512            Object key = entry.getKey();
513            Object val = entry.getValue();
514
515            String keyStr = getStringValue(key);
516            String elemPropertyName = propertyName + "['" + keyStr + "']";
517
518            if (hasExpression(val)) {
519                String strValue = getStringValue(val);
520                propertyExpressions.put(elemPropertyName, strValue);
521                newContent.put(key, null);
522            } else {
523                Object newElem = processPropertyValue(elemPropertyName, val, parentPropertyExpressions,
524                        propertyExpressions, beanFactory, processedBeanNames);
525                newContent.put(key, newElem);
526            }
527
528            if (isMergeEnabled && parentPropertyExpressions.containsKey(elemPropertyName)) {
529                parentPropertyExpressions.remove(elemPropertyName);
530            }
531        }
532
533        if (!isMergeEnabled) {
534            // clear any expressions that match the property minus key
535            Map<String, String> adjustedParentExpressions = new HashMap<String, String>();
536            for (Map.Entry<String, String> parentExpression : parentPropertyExpressions.entrySet()) {
537                if (!parentExpression.getKey().startsWith(propertyName + "[")) {
538                    adjustedParentExpressions.put(parentExpression.getKey(), parentExpression.getValue());
539                }
540            }
541
542            parentPropertyExpressions.clear();
543            parentPropertyExpressions.putAll(adjustedParentExpressions);
544        }
545
546        mapVal.clear();
547        mapVal.putAll(newContent);
548    }
549}