001    /**
002     * Copyright 2005-2014 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     */
016    package org.kuali.rice.krad.uif.util;
017    
018    import org.apache.commons.lang.StringUtils;
019    import org.apache.commons.logging.Log;
020    import org.apache.commons.logging.LogFactory;
021    import org.kuali.rice.krad.uif.UifConstants;
022    import org.kuali.rice.krad.uif.UifPropertyPaths;
023    import org.kuali.rice.krad.uif.component.Configurable;
024    import org.springframework.beans.BeansException;
025    import org.springframework.beans.MutablePropertyValues;
026    import org.springframework.beans.PropertyValue;
027    import org.springframework.beans.factory.config.BeanDefinition;
028    import org.springframework.beans.factory.config.BeanDefinitionHolder;
029    import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
030    import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
031    import org.springframework.beans.factory.config.TypedStringValue;
032    import org.springframework.beans.factory.support.BeanDefinitionRegistry;
033    import org.springframework.beans.factory.support.GenericBeanDefinition;
034    import org.springframework.beans.factory.support.ManagedList;
035    import org.springframework.beans.factory.support.ManagedMap;
036    
037    import java.util.ArrayList;
038    import java.util.HashMap;
039    import java.util.HashSet;
040    import java.util.LinkedHashMap;
041    import java.util.LinkedHashSet;
042    import java.util.List;
043    import java.util.Map;
044    import 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     */
065    public 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    }