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 }