View Javadoc

1   /**
2    * Copyright 2005-2011 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.uif.util;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.apache.commons.logging.Log;
20  import org.apache.commons.logging.LogFactory;
21  import org.kuali.rice.krad.datadictionary.DataDictionary;
22  import org.kuali.rice.krad.uif.UifConstants;
23  import org.kuali.rice.krad.uif.UifPropertyPaths;
24  import org.kuali.rice.krad.uif.component.Configurable;
25  import org.springframework.beans.BeansException;
26  import org.springframework.beans.MutablePropertyValues;
27  import org.springframework.beans.PropertyValue;
28  import org.springframework.beans.factory.config.BeanDefinition;
29  import org.springframework.beans.factory.config.BeanDefinitionHolder;
30  import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
31  import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
32  import org.springframework.beans.factory.config.TypedStringValue;
33  import org.springframework.beans.factory.support.GenericBeanDefinition;
34  import org.springframework.beans.factory.support.ManagedList;
35  import org.springframework.beans.factory.support.ManagedMap;
36  
37  import java.util.ArrayList;
38  import java.util.HashMap;
39  import java.util.HashSet;
40  import java.util.LinkedHashMap;
41  import java.util.LinkedHashSet;
42  import java.util.List;
43  import java.util.Map;
44  import java.util.Set;
45  
46  /**
47   * Post processes the bean factory to handle UIF property expressions
48   *
49   * <p>
50   * Conditional logic can be implemented with the UIF dictionary by means of property expressions. These are
51   * expressions that follow SPEL and can be given as the value for a property using the @{} placeholder. Since such
52   * a value would cause an exception when creating the object if the property is a non-string type (value cannot be
53   * converted), we need to move those expressions to a Map for processing, and then remove the original property
54   * configuration containing the expression. The expressions are then evaluated during the view apply model phase and
55   * the result is set as the value for the corresponding property.
56   * </p>
57   *
58   * @author Kuali Rice Team (rice.collab@kuali.org)
59   */
60  public class UifBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
61      private static final Log LOG = LogFactory.getLog(UifBeanFactoryPostProcessor.class);
62  
63      /**
64       * Iterates through all beans in the factory and invokes processing for expressions
65       *
66       * @param beanFactory - bean factory instance to process
67       * @throws BeansException
68       */
69      @Override
70      public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
71          Set<String> processedBeanNames = new HashSet<String>();
72  
73          LOG.info("Beginning post processing of bean factory for UIF expressions");
74  
75          String[] beanNames = beanFactory.getBeanDefinitionNames();
76          for (int i = 0; i < beanNames.length; i++) {
77              String beanName = beanNames[i];
78              BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
79  
80              processBeanDefinition(beanName, beanDefinition, beanFactory, processedBeanNames);
81          }
82  
83          LOG.info("Finished post processing of bean factory for UIF expressions");
84      }
85  
86      /**
87       * If the bean class is type Component, LayoutManager, or BindingInfo, iterate through configured property values
88       * and check for expressions
89       *
90       * <p>
91       * If a expression is found for a property, it is added to the 'propertyExpressions' map and then the original
92       * property value is removed to prevent binding errors (when converting to a non string type)
93       * </p>
94       *
95       * @param beanName - name of the bean in the factory (only set for top level beans, not nested)
96       * @param beanDefinition - bean definition to process for expressions
97       * @param beanFactory - bean factory being processed
98       */
99      protected void processBeanDefinition(String beanName, BeanDefinition beanDefinition,
100             ConfigurableListableBeanFactory beanFactory, Set<String> processedBeanNames) {
101         Class<?> beanClass = getBeanClass(beanDefinition, beanFactory);
102         if ((beanClass == null) || !Configurable.class.isAssignableFrom(beanClass)) {
103             return;
104         }
105 
106         if (processedBeanNames.contains(beanName)) {
107             return;
108         }
109 
110         LOG.debug("Processing bean name '" + beanName + "'");
111 
112         MutablePropertyValues pvs = beanDefinition.getPropertyValues();
113 
114         if (pvs.getPropertyValue(UifPropertyPaths.PROPERTY_EXPRESSIONS) != null) {
115             // already processed so skip (could be reloading dictionary)
116             return;
117         }
118 
119         Map<String, String> propertyExpressions = new ManagedMap<String, String>();
120         Map<String, String> parentPropertyExpressions = getPropertyExpressionsFromParent(beanDefinition.getParentName(),
121                 beanFactory, processedBeanNames);
122         boolean parentExpressionsExist = !parentPropertyExpressions.isEmpty();
123 
124         PropertyValue[] pvArray = pvs.getPropertyValues();
125         for (PropertyValue pv : pvArray) {
126             if (hasExpression(pv.getValue())) {
127                 // process expression
128                 String strValue = getStringValue(pv.getValue());
129                 propertyExpressions.put(pv.getName(), strValue);
130 
131                 // remove property value so expression will not cause binding exception
132                 pvs.removePropertyValue(pv.getName());
133             } else {
134                 // process nested objects
135                 Object newValue = processPropertyValue(pv.getName(), pv.getValue(), parentPropertyExpressions,
136                         propertyExpressions, beanFactory, processedBeanNames);
137                 pvs.removePropertyValue(pv.getName());
138                 pvs.addPropertyValue(pv.getName(), newValue);
139             }
140 
141             // removed expression (if exists) from parent map since the property was set on child
142             if (parentPropertyExpressions.containsKey(pv.getName())) {
143                 parentPropertyExpressions.remove(pv.getName());
144             }
145 
146             // if property is nested, need to override any parent expressions set on nested beans
147             if (StringUtils.contains(pv.getName(), ".")) {
148                 //removeParentExpressionsOnNested(pv.getName(), pvs, beanDefinition.getParentName(), beanFactory);
149             }
150         }
151 
152         if (!propertyExpressions.isEmpty() || parentExpressionsExist) {
153             // merge two maps
154             ManagedMap<String, String> mergedPropertyExpressions = new ManagedMap<String, String>();
155             mergedPropertyExpressions.setMergeEnabled(false);
156             mergedPropertyExpressions.putAll(parentPropertyExpressions);
157             mergedPropertyExpressions.putAll(propertyExpressions);
158 
159             pvs.addPropertyValue(UifPropertyPaths.PROPERTY_EXPRESSIONS, mergedPropertyExpressions);
160         }
161 
162         if (StringUtils.isNotBlank(beanName)) {
163             processedBeanNames.add(beanName);
164         }
165     }
166 
167     protected void removeParentExpressionsOnNested(String propertyName, MutablePropertyValues pvs,
168             String parentBeanName, ConfigurableListableBeanFactory beanFactory) {
169         BeanDefinition parentBeanDefinition = beanFactory.getMergedBeanDefinition(parentBeanName);
170 
171         // TODO: this only handles one level of nesting
172         MutablePropertyValues parentPvs = parentBeanDefinition.getPropertyValues();
173         PropertyValue[] pvArray = parentPvs.getPropertyValues();
174         for (PropertyValue pv : pvArray) {
175             boolean isNameMatch = false;
176             String nestedPropertyName = "";
177             if (propertyName.startsWith(pv.getName())) {
178                 nestedPropertyName = StringUtils.removeStart(propertyName, pv.getName());
179                 if (nestedPropertyName.startsWith(".")) {
180                     nestedPropertyName = StringUtils.removeStart(nestedPropertyName, ".");
181                     isNameMatch = true;
182                 }
183             }
184 
185             // if property name from parent matches and is a bean definition, check for property expressions map
186             if (isNameMatch && ((pv.getValue() instanceof BeanDefinition) || (pv
187                     .getValue() instanceof BeanDefinitionHolder))) {
188                 BeanDefinition propertyBeanDefinition;
189                 if (pv.getValue() instanceof BeanDefinition) {
190                     propertyBeanDefinition = (BeanDefinition) pv.getValue();
191                 } else {
192                     propertyBeanDefinition = ((BeanDefinitionHolder) pv.getValue()).getBeanDefinition();
193                 }
194 
195                 MutablePropertyValues nestedPvs = propertyBeanDefinition.getPropertyValues();
196                 if (nestedPvs.contains(UifPropertyPaths.PROPERTY_EXPRESSIONS)) {
197                     PropertyValue propertyExpressionsPV = nestedPvs.getPropertyValue(
198                             UifPropertyPaths.PROPERTY_EXPRESSIONS);
199                     if (propertyExpressionsPV != null) {
200                         Object value = propertyExpressionsPV.getValue();
201                         if ((value != null) && (value instanceof ManagedMap)) {
202                             Map<String, String> nestedPropertyExpressions = (ManagedMap) value;
203                             if (nestedPropertyExpressions.containsKey(nestedPropertyName)) {
204                                 // need to make copy of property value with expression removed from map
205                                 ManagedMap<String, String> copiedPropertyExpressions = new ManagedMap<String, String>();
206                                 copiedPropertyExpressions.setMergeEnabled(false);
207                                 copiedPropertyExpressions.putAll(nestedPropertyExpressions);
208                                 copiedPropertyExpressions.remove(nestedPropertyName);
209 
210                                 BeanDefinition copiedBeanDefinition = new GenericBeanDefinition(propertyBeanDefinition);
211                                 copiedBeanDefinition.getPropertyValues().add(UifPropertyPaths.PROPERTY_EXPRESSIONS,
212                                         copiedPropertyExpressions);
213 
214                                 pvs.add(pv.getName(), copiedBeanDefinition);
215                             }
216                         }
217                     }
218                 }
219             }
220         }
221     }
222 
223     /**
224      * Retrieves the class for the object that will be created from the bean definition. Since the class might not
225      * be configured on the bean definition, but by a parent, each parent bean definition is recursively checked for
226      * a class until one is found
227      *
228      * @param beanDefinition - bean definition to get class for
229      * @param beanFactory - bean factory that contains the bean definition
230      * @return Class<?> class configured for the bean definition, or null
231      */
232     protected Class<?> getBeanClass(BeanDefinition beanDefinition, ConfigurableListableBeanFactory beanFactory) {
233         if (StringUtils.isNotBlank(beanDefinition.getBeanClassName())) {
234             try {
235                 return Class.forName(beanDefinition.getBeanClassName());
236             } catch (ClassNotFoundException e) {
237                 // swallow exception and return null so bean is not processed
238                 return null;
239             }
240         } else if (StringUtils.isNotBlank(beanDefinition.getParentName())) {
241             BeanDefinition parentBeanDefinition = beanFactory.getBeanDefinition(beanDefinition.getParentName());
242             if (parentBeanDefinition != null) {
243                 return getBeanClass(parentBeanDefinition, beanFactory);
244             }
245         }
246 
247         return null;
248     }
249 
250     /**
251      * Retrieves the property expressions map set on the bean with given name. If the bean has not been processed
252      * by the bean factory post processor, that is done before retrieving the map
253      *
254      * @param parentBeanName - name of the parent bean to retrieve map for (if empty a new map will be returned)
255      * @param beanFactory - bean factory to retrieve bean definition from
256      * @param processedBeanNames - set of bean names that have been processed so far
257      * @return Map<String, String> property expressions map from parent or new instance
258      */
259     protected Map<String, String> getPropertyExpressionsFromParent(String parentBeanName,
260             ConfigurableListableBeanFactory beanFactory, Set<String> processedBeanNames) {
261         Map<String, String> propertyExpressions = new HashMap<String, String>();
262         if (StringUtils.isBlank(parentBeanName) || !beanFactory.containsBeanDefinition(parentBeanName)) {
263             return propertyExpressions;
264         }
265 
266         if (!processedBeanNames.contains(parentBeanName)) {
267             processBeanDefinition(parentBeanName, beanFactory.getBeanDefinition(parentBeanName), beanFactory,
268                     processedBeanNames);
269         }
270 
271         BeanDefinition beanDefinition = beanFactory.getBeanDefinition(parentBeanName);
272         MutablePropertyValues pvs = beanDefinition.getPropertyValues();
273 
274         PropertyValue propertyExpressionsPV = pvs.getPropertyValue(UifPropertyPaths.PROPERTY_EXPRESSIONS);
275         if (propertyExpressionsPV != null) {
276             Object value = propertyExpressionsPV.getValue();
277             if ((value != null) && (value instanceof ManagedMap)) {
278                 propertyExpressions.putAll((ManagedMap) value);
279             }
280         }
281 
282         return propertyExpressions;
283     }
284 
285     /**
286      * Checks whether the given property value is of String type, and if so whether it contains the expression
287      * placholder(s)
288      *
289      * @param propertyValue - value to check for expressions
290      * @return boolean true if the property value contains expression(s), false if it does not
291      */
292     protected boolean hasExpression(Object propertyValue) {
293         if (propertyValue != null) {
294             // if value is string, check for el expression
295             String strValue = getStringValue(propertyValue);
296             if (strValue != null) {
297                 String elPlaceholder = StringUtils.substringBetween(strValue, UifConstants.EL_PLACEHOLDER_PREFIX,
298                         UifConstants.EL_PLACEHOLDER_SUFFIX);
299                 if (StringUtils.isNotBlank(elPlaceholder)) {
300                     return true;
301                 }
302             }
303         }
304 
305         return false;
306     }
307 
308     /**
309      * Processes the given property name/value pair for complex objects, such as bean definitions or collections,
310      * which if found will be processed for contained property expression values
311      *
312      * @param propertyName - name of the property whose value is being processed
313      * @param propertyValue - value to check
314      * @param parentPropertyExpressions - map that holds property expressions for the parent bean definition, used for
315      * merging
316      * @param propertyExpressions - map that holds property expressions for the bean definition being processed
317      * @param beanFactory - bean factory that contains the bean definition being processed
318      * @param processedBeanNames - set of bean names that have been processed so far
319      * @return Object new value to set for property
320      */
321     protected Object processPropertyValue(String propertyName, Object propertyValue,
322             Map<String, String> parentPropertyExpressions, Map<String, String> propertyExpressions,
323             ConfigurableListableBeanFactory beanFactory, Set<String> processedBeanNames) {
324         if (propertyValue == null) {
325             return null;
326         }
327 
328         // process nested bean definitions
329         if ((propertyValue instanceof BeanDefinition) || (propertyValue instanceof BeanDefinitionHolder)) {
330             BeanDefinition beanDefinition;
331             if (propertyValue instanceof BeanDefinition) {
332                 beanDefinition = (BeanDefinition) propertyValue;
333             } else {
334                 beanDefinition = ((BeanDefinitionHolder) propertyValue).getBeanDefinition();
335             }
336 
337             // since overriding the entire bean, clear any expressions from parent that start with the bean property
338             removeExpressionsByPrefix(propertyName, parentPropertyExpressions);
339             processBeanDefinition(null, beanDefinition, beanFactory, processedBeanNames);
340 
341             return propertyValue;
342         }
343 
344         // recurse into collections
345         if (propertyValue instanceof Object[]) {
346             visitArray(propertyName, parentPropertyExpressions, propertyExpressions, (Object[]) propertyValue,
347                     beanFactory, processedBeanNames);
348         } else if (propertyValue instanceof List) {
349             visitList(propertyName, parentPropertyExpressions, propertyExpressions, (List) propertyValue, beanFactory,
350                     processedBeanNames);
351         } else if (propertyValue instanceof Set) {
352             visitSet(propertyName, parentPropertyExpressions, propertyExpressions, (Set) propertyValue, beanFactory,
353                     processedBeanNames);
354         } else if (propertyValue instanceof Map) {
355             visitMap(propertyName, parentPropertyExpressions, propertyExpressions, (Map) propertyValue, beanFactory,
356                     processedBeanNames);
357         }
358 
359         // others (primitive) just return value as is
360         return propertyValue;
361     }
362 
363     /**
364      * Removes entries from the given expressions map whose key starts with the given prefix
365      *
366      * @param propertyNamePrefix - prefix to search for and remove
367      * @param propertyExpressions - map of property expressions to filter
368      */
369     protected void removeExpressionsByPrefix(String propertyNamePrefix, Map<String, String> propertyExpressions) {
370         Map<String, String> adjustedPropertyExpressions = new HashMap<String, String>();
371         for (String propertyName : propertyExpressions.keySet()) {
372             if (!propertyName.startsWith(propertyNamePrefix)) {
373                 adjustedPropertyExpressions.put(propertyName, propertyExpressions.get(propertyName));
374             }
375         }
376 
377         propertyExpressions.clear();
378         propertyExpressions.putAll(adjustedPropertyExpressions);
379     }
380 
381     /**
382      * Determines whether the given value is of String type and if so returns the string value
383      *
384      * @param value - object value to check
385      * @return String string value for object or null if object is not a string type
386      */
387     protected String getStringValue(Object value) {
388         if (value instanceof TypedStringValue) {
389             TypedStringValue typedStringValue = (TypedStringValue) value;
390             return typedStringValue.getValue();
391         } else if (value instanceof String) {
392             return (String) value;
393         }
394 
395         return null;
396     }
397 
398     @SuppressWarnings("unchecked")
399     protected void visitArray(String propertyName, Map<String, String> parentPropertyExpressions,
400             Map<String, String> propertyExpressions, Object[] arrayVal, ConfigurableListableBeanFactory beanFactory,
401             Set<String> processedBeanNames) {
402         for (int i = 0; i < arrayVal.length; i++) {
403             Object elem = arrayVal[i];
404             String elemPropertyName = propertyName + "[" + i + "]";
405 
406             if (hasExpression(elem)) {
407                 String strValue = getStringValue(elem);
408                 propertyExpressions.put(elemPropertyName, strValue);
409                 arrayVal[i] = null;
410             } else {
411                 Object newElem = processPropertyValue(elemPropertyName, elem, parentPropertyExpressions,
412                         propertyExpressions, beanFactory, processedBeanNames);
413                 arrayVal[i] = newElem;
414             }
415 
416             if (parentPropertyExpressions.containsKey(elemPropertyName)) {
417                 parentPropertyExpressions.remove(elemPropertyName);
418             }
419         }
420     }
421 
422     @SuppressWarnings("unchecked")
423     protected void visitList(String propertyName, Map<String, String> parentPropertyExpressions,
424             Map<String, String> propertyExpressions, List listVal, ConfigurableListableBeanFactory beanFactory,
425             Set<String> processedBeanNames) {
426         List newList = new ArrayList();
427 
428         for (int i = 0; i < listVal.size(); i++) {
429             Object elem = listVal.get(i);
430             String elemPropertyName = propertyName + "[" + i + "]";
431 
432             if (hasExpression(elem)) {
433                 String strValue = getStringValue(elem);
434                 propertyExpressions.put(elemPropertyName, strValue);
435                 newList.add(i, null);
436             } else {
437                 Object newElem = processPropertyValue(elemPropertyName, elem, parentPropertyExpressions,
438                         propertyExpressions, beanFactory, processedBeanNames);
439                 newList.add(i, newElem);
440             }
441 
442             if (parentPropertyExpressions.containsKey(elemPropertyName)) {
443                 parentPropertyExpressions.remove(elemPropertyName);
444             }
445         }
446 
447         // determine if we need to clear any parent expressions for this list
448         if (listVal instanceof ManagedList) {
449             boolean isMergeEnabled = ((ManagedList) listVal).isMergeEnabled();
450             if (!isMergeEnabled) {
451                 // clear any expressions that match the property name minus index
452                 Map<String, String> adjustedParentExpressions = new HashMap<String, String>();
453                 for (Map.Entry<String, String> parentExpression : parentPropertyExpressions.entrySet()) {
454                     if (!parentExpression.getKey().startsWith(propertyName + "[")) {
455                         adjustedParentExpressions.put(parentExpression.getKey(), parentExpression.getValue());
456                     }
457                 }
458 
459                 parentPropertyExpressions.clear();
460                 parentPropertyExpressions.putAll(adjustedParentExpressions);
461             }
462         }
463 
464         listVal.clear();
465         listVal.addAll(newList);
466     }
467 
468     @SuppressWarnings("unchecked")
469     protected void visitSet(String propertyName, Map<String, String> parentPropertyExpressions,
470             Map<String, String> propertyExpressions, Set setVal, ConfigurableListableBeanFactory beanFactory,
471             Set<String> processedBeanNames) {
472         Set newContent = new LinkedHashSet();
473 
474         // TODO: this is not handled correctly
475         for (Object elem : setVal) {
476             Object newElem = processPropertyValue(propertyName, elem, parentPropertyExpressions, propertyExpressions,
477                     beanFactory, processedBeanNames);
478             newContent.add(newElem);
479         }
480 
481         setVal.clear();
482         setVal.addAll(newContent);
483     }
484 
485     @SuppressWarnings("unchecked")
486     protected void visitMap(String propertyName, Map<String, String> parentPropertyExpressions,
487             Map<String, String> propertyExpressions, Map<?, ?> mapVal, ConfigurableListableBeanFactory beanFactory,
488             Set<String> processedBeanNames) {
489         Map newContent = new LinkedHashMap();
490 
491         boolean isMergeEnabled = false;
492         if (mapVal instanceof ManagedMap) {
493             isMergeEnabled = ((ManagedMap) mapVal).isMergeEnabled();
494         }
495 
496         for (Map.Entry entry : mapVal.entrySet()) {
497             Object key = entry.getKey();
498             Object val = entry.getValue();
499 
500             String keyStr = getStringValue(key);
501             String elemPropertyName = propertyName + "['" + keyStr + "']";
502 
503             if (hasExpression(val)) {
504                 String strValue = getStringValue(val);
505                 propertyExpressions.put(elemPropertyName, strValue);
506                 newContent.put(key, null);
507             } else {
508                 Object newElem = processPropertyValue(elemPropertyName, val, parentPropertyExpressions,
509                         propertyExpressions, beanFactory, processedBeanNames);
510                 newContent.put(key, newElem);
511             }
512 
513             if (isMergeEnabled && parentPropertyExpressions.containsKey(elemPropertyName)) {
514                 parentPropertyExpressions.remove(elemPropertyName);
515             }
516         }
517 
518         if (!isMergeEnabled) {
519             // clear any expressions that match the property minus key
520             Map<String, String> adjustedParentExpressions = new HashMap<String, String>();
521             for (Map.Entry<String, String> parentExpression : parentPropertyExpressions.entrySet()) {
522                 if (!parentExpression.getKey().startsWith(propertyName + "[")) {
523                     adjustedParentExpressions.put(parentExpression.getKey(), parentExpression.getValue());
524                 }
525             }
526 
527             parentPropertyExpressions.clear();
528             parentPropertyExpressions.putAll(adjustedParentExpressions);
529         }
530 
531         mapVal.clear();
532         mapVal.putAll(newContent);
533     }
534 }