View Javadoc
1   /**
2    * Copyright 2005-2016 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.uif.UifConstants;
22  import org.kuali.rice.krad.uif.UifPropertyPaths;
23  import org.kuali.rice.krad.uif.component.Configurable;
24  import org.springframework.beans.BeansException;
25  import org.springframework.beans.MutablePropertyValues;
26  import org.springframework.beans.PropertyValue;
27  import org.springframework.beans.factory.config.BeanDefinition;
28  import org.springframework.beans.factory.config.BeanDefinitionHolder;
29  import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
30  import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
31  import org.springframework.beans.factory.config.TypedStringValue;
32  import org.springframework.beans.factory.support.BeanDefinitionRegistry;
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 and IDs on inner beans
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   * <p>
59   * Spring will not register inner beans with IDs so that the bean definition can be retrieved through the factory,
60   * therefore this post processor adds them as top level registered beans
61   * </p>
62   *
63   * @author Kuali Rice Team (rice.collab@kuali.org)
64   */
65  public class UifBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
66      private static final Log LOG = LogFactory.getLog(UifBeanFactoryPostProcessor.class);
67  
68      /**
69       * Iterates through all beans in the factory and invokes processing
70       *
71       * @param beanFactory - bean factory instance to process
72       * @throws BeansException
73       */
74      @Override
75      public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
76          Set<String> processedBeanNames = new HashSet<String>();
77  
78          LOG.info("Beginning post processing of bean factory for UIF expressions");
79  
80          String[] beanNames = beanFactory.getBeanDefinitionNames();
81          for (int i = 0; i < beanNames.length; i++) {
82              String beanName = beanNames[i];
83              BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
84  
85              processBeanDefinition(beanName, beanDefinition, beanFactory, processedBeanNames);
86          }
87  
88          LOG.info("Finished post processing of bean factory for UIF expressions");
89      }
90  
91      /**
92       * If the bean class is type Component, LayoutManager, or BindingInfo, iterate through configured property values
93       * and check for expressions
94       *
95       * <p>
96       * If a expression is found for a property, it is added to the 'propertyExpressions' map and then the original
97       * property value is removed to prevent binding errors (when converting to a non string type)
98       * </p>
99       *
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 }