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 }