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.kuali.rice.core.api.util.type.TypeUtils; 020 import org.kuali.rice.krad.uif.UifConstants; 021 import org.kuali.rice.krad.uif.container.Container; 022 import org.kuali.rice.krad.uif.component.Component; 023 import org.kuali.rice.krad.uif.component.DataBinding; 024 import org.kuali.rice.krad.uif.component.Ordered; 025 import org.kuali.rice.krad.uif.container.ContainerBase; 026 import org.kuali.rice.krad.uif.field.Field; 027 import org.kuali.rice.krad.uif.field.FieldGroup; 028 import org.kuali.rice.krad.uif.field.InputField; 029 import org.kuali.rice.krad.uif.layout.LayoutManager; 030 import org.kuali.rice.krad.util.ObjectUtils; 031 import org.springframework.beans.BeanUtils; 032 import org.springframework.core.OrderComparator; 033 034 import java.beans.PropertyDescriptor; 035 import java.io.Serializable; 036 import java.util.ArrayList; 037 import java.util.Collections; 038 import java.util.HashSet; 039 import java.util.List; 040 import java.util.Map; 041 import java.util.Set; 042 043 /** 044 * ComponentUtils is a utility class providing methods to help create and modify <code>Component</code> instances 045 * 046 * @author Kuali Rice Team (rice.collab@kuali.org) 047 */ 048 public class ComponentUtils { 049 private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(ComponentUtils.class); 050 051 052 public static <T extends Component> T copy(T component) { 053 return copy(component, null); 054 } 055 056 public static <T extends Component> T copy(T component, String idSuffix) { 057 T copy = copyObject(component); 058 059 if (StringUtils.isNotBlank(idSuffix)) { 060 updateIdsWithSuffixNested(copy, idSuffix); 061 } 062 063 return copy; 064 } 065 066 public static <T extends Object> T copyObject(T object) { 067 if (object == null) { 068 return null; 069 } 070 071 T copy = null; 072 try { 073 copy = CloneUtils.deepClone(object); 074 } 075 catch (Exception e) { 076 throw new RuntimeException(e); 077 } 078 079 return copy; 080 } 081 082 protected static Object getCopyPropertyValue(Set<String> propertiesForReferenceCopy, String propertyName, 083 Object propertyValue) { 084 if (propertyValue == null) { 085 return null; 086 } 087 088 Object copyValue = propertyValue; 089 090 Class<?> valuePropertyType = propertyValue.getClass(); 091 if (propertiesForReferenceCopy.contains(propertyName) || TypeUtils.isSimpleType(valuePropertyType) 092 || TypeUtils.isClassClass(valuePropertyType)) { 093 return copyValue; 094 } 095 096 if (Component.class.isAssignableFrom(valuePropertyType) 097 || LayoutManager.class.isAssignableFrom(valuePropertyType)) { 098 copyValue = copyObject(propertyValue); 099 } 100 else { 101 copyValue = ObjectUtils.deepCopy((Serializable) propertyValue); 102 } 103 104 return copyValue; 105 } 106 107 @SuppressWarnings("unchecked") 108 protected static <T extends Object> T getNewInstance(T object) { 109 T copy = null; 110 try { 111 copy = (T) object.getClass().newInstance(); 112 } 113 catch (Exception e) { 114 throw new RuntimeException("Unable to create new instance of class: " + object.getClass()); 115 } 116 117 return copy; 118 } 119 120 public static <T extends Field> List<T> copyFieldList(List<T> fields, String addBindingPrefix, String idSuffix) { 121 List<T> copiedFieldList = copyFieldList(fields, idSuffix); 122 123 prefixBindingPath(copiedFieldList, addBindingPrefix); 124 125 return copiedFieldList; 126 } 127 128 public static <T extends Field> List<T> copyFieldList(List<T> fields, String idSuffix) { 129 List<T> copiedFieldList = new ArrayList<T>(); 130 131 for (T field : fields) { 132 T copiedField = copy(field, idSuffix); 133 copiedFieldList.add(copiedField); 134 } 135 136 return copiedFieldList; 137 } 138 139 public static <T extends Component> T copyComponent(T component, String addBindingPrefix, String idSuffix) { 140 T copy = copy(component, idSuffix); 141 142 prefixBindingPathNested(copy, addBindingPrefix); 143 144 return copy; 145 } 146 147 public static <T extends Component> List<T> copyComponentList(List<T> components, String idSuffix) { 148 List<T> copiedComponentList = new ArrayList<T>(); 149 150 for (T field : components) { 151 T copiedComponent = copy(field, idSuffix); 152 copiedComponentList.add(copiedComponent); 153 } 154 155 return copiedComponentList; 156 } 157 158 @SuppressWarnings("unchecked") 159 public static <T extends Component> List<T> getComponentsOfType(List<? extends Component> items, 160 Class<T> componentType) { 161 List<T> typeComponents = new ArrayList<T>(); 162 163 for (Component component : items) { 164 if (componentType.isAssignableFrom(component.getClass())) { 165 typeComponents.add((T) component); 166 } 167 } 168 169 return typeComponents; 170 } 171 172 /** 173 * Return the components of the specified type from the given component list 174 * 175 * <p> 176 * Components that match, implement or are extended from the specified {@code componentType} are returned in 177 * the result. If a component is a parent to other components then these child components are searched for 178 * matching component types as well. 179 * </p> 180 * 181 * @param items list of components from which to search 182 * @param componentType the class or interface of the component type to return 183 * @param <T> the type of the components that are returned 184 * @return List of matching components 185 */ 186 public static <T extends Component> List<T> getComponentsOfTypeDeep(List<? extends Component> items, 187 Class<T> componentType) { 188 List<T> typeComponents = new ArrayList<T>(); 189 190 for (Component component : items) { 191 typeComponents.addAll(getComponentsOfTypeDeep(component, componentType)); 192 } 193 194 return typeComponents; 195 } 196 197 @SuppressWarnings("unchecked") 198 public static <T extends Component> List<T> getComponentsOfTypeDeep(Component component, Class<T> componentType) { 199 List<T> typeComponents = new ArrayList<T>(); 200 201 if (component == null) { 202 return typeComponents; 203 } 204 205 if (componentType.isAssignableFrom(component.getClass())) { 206 typeComponents.add((T) component); 207 } 208 209 for (Component nested : component.getComponentsForLifecycle()) { 210 typeComponents.addAll(getComponentsOfTypeDeep(nested, componentType)); 211 } 212 213 return typeComponents; 214 } 215 216 public static List<Component> getAllNestedComponents(Component component) { 217 List<Component> components = new ArrayList<Component>(); 218 219 if (component == null) { 220 return components; 221 } 222 223 for (Component nested : component.getComponentsForLifecycle()) { 224 if (nested != null) { 225 components.add(nested); 226 components.addAll(getAllNestedComponents(nested)); 227 } 228 } 229 230 return components; 231 } 232 233 /** 234 * Searches for the component with the given id within the given list of components 235 * 236 * @param components list of components to search through 237 * @param componentId id for the component to find 238 * @return Component component found in the list or null 239 */ 240 public static Component findComponentInList(List<Component> components, String componentId) { 241 Component foundComponent = null; 242 243 for (Component component : components) { 244 if (component != null && component.getId() != null && component.getId().equals(componentId)) { 245 foundComponent = component; 246 break; 247 } 248 } 249 250 return foundComponent; 251 } 252 253 /** 254 * Finds the child component of the given parent component that has the required id 255 * 256 * @param parent - parent component for component to find 257 * @param nestedId - id of the component to find 258 * @return Component instance for child (if found) or null 259 */ 260 public static Component findNestedComponentById(Component parent, String nestedId) { 261 Component childComponent = null; 262 263 List<Component> children = getAllNestedComponents(parent); 264 for (Component child : children) { 265 if (StringUtils.equals(nestedId, child.getId())) { 266 childComponent = child; 267 break; 268 } 269 } 270 271 return childComponent; 272 } 273 274 public static void prefixBindingPath(List<? extends Field> fields, String addBindingPrefix) { 275 for (Field field : fields) { 276 if (field instanceof DataBinding) { 277 prefixBindingPath((DataBinding) field, addBindingPrefix); 278 } 279 else if ((field instanceof FieldGroup) && (((FieldGroup) field).getItems() != null) ) { 280 List<Field> groupFields = getComponentsOfTypeDeep(((FieldGroup) field).getItems(), Field.class); 281 prefixBindingPath(groupFields, addBindingPrefix); 282 } 283 } 284 } 285 286 public static void prefixBindingPathNested(Component component, String addBindingPrefix) { 287 if (component instanceof DataBinding) { 288 if (LOG.isDebugEnabled()) { 289 LOG.info("setting nested binding prefix '"+ addBindingPrefix +"' on " + component); 290 } 291 prefixBindingPath((DataBinding) component, addBindingPrefix); 292 } 293 294 for (Component nested : component.getComponentsForLifecycle()) { 295 if (nested != null) { 296 prefixBindingPathNested(nested, addBindingPrefix); 297 } 298 } 299 } 300 301 public static void prefixBindingPath(DataBinding field, String addBindingPrefix) { 302 String bindingPrefix = addBindingPrefix; 303 if (StringUtils.isNotBlank(field.getBindingInfo().getBindByNamePrefix())) { 304 bindingPrefix += "." + field.getBindingInfo().getBindByNamePrefix(); 305 } 306 field.getBindingInfo().setBindByNamePrefix(bindingPrefix); 307 } 308 309 public static void updateIdsWithSuffixNested(List<? extends Component> components, String idSuffix) { 310 for (Component component : components) { 311 updateIdsWithSuffixNested(component, idSuffix); 312 } 313 } 314 315 public static void updateIdsWithSuffixNested(Component component, String idSuffix) { 316 updateIdWithSuffix(component, idSuffix); 317 318 updateChildIdsWithSuffixNested(component, idSuffix); 319 } 320 321 public static void updateChildIdsWithSuffixNested(Component component, String idSuffix) { 322 if (Container.class.isAssignableFrom(component.getClass())) { 323 LayoutManager layoutManager = ((Container) component).getLayoutManager(); 324 layoutManager.setId(layoutManager.getId() + idSuffix); 325 } 326 327 for (Component nested : component.getComponentsForLifecycle()) { 328 if (nested != null) { 329 updateIdsWithSuffixNested(nested, idSuffix); 330 } 331 } 332 333 for (Component nested : component.getPropertyReplacerComponents()) { 334 if (nested != null) { 335 updateIdsWithSuffixNested(nested, idSuffix); 336 } 337 } 338 } 339 340 /** 341 * add a suffix to the id 342 * 343 * @param component - the component instance whose id will be changed 344 * @param idSuffix - the suffix to be appended 345 */ 346 public static void updateIdWithSuffix(Component component, String idSuffix) { 347 if (component != null && !StringUtils.isEmpty(idSuffix)) { 348 component.setId(component.getId() + idSuffix); 349 } 350 } 351 352 public static void setComponentsPropertyDeep(List<? extends Component> components, String propertyPath, 353 Object propertyValue) { 354 for (Component component : components) { 355 setComponentPropertyDeep(component, propertyPath, propertyValue); 356 } 357 } 358 359 public static void setComponentPropertyDeep(Component component, String propertyPath, Object propertyValue) { 360 ObjectPropertyUtils.setPropertyValue(component, propertyPath, propertyValue, true); 361 362 for (Component nested : component.getComponentsForLifecycle()) { 363 if (nested != null) { 364 setComponentPropertyDeep(nested, propertyPath, propertyValue); 365 } 366 } 367 } 368 369 public static List<String> getComponentPropertyNames(Class<? extends Component> componentClass) { 370 List<String> componentProperties = new ArrayList<String>(); 371 372 PropertyDescriptor[] properties = BeanUtils.getPropertyDescriptors(componentClass); 373 for (int i = 0; i < properties.length; i++) { 374 PropertyDescriptor descriptor = properties[i]; 375 if (descriptor.getReadMethod() != null) { 376 componentProperties.add(descriptor.getName()); 377 } 378 } 379 380 return componentProperties; 381 } 382 383 /** 384 * places a key, value pair in each context map of a list of components 385 * 386 * @param components - the list components 387 * @param contextName - a value to be used as a key to retrieve the object 388 * @param contextValue - the value to be placed in the context 389 */ 390 public static void pushObjectToContext(List<? extends Component> components, String contextName, Object contextValue) { 391 for (Component component : components) { 392 pushObjectToContext(component, contextName, contextValue); 393 } 394 } 395 396 /** 397 * pushes object to a component's context so that it is available from {@link Component#getContext()} 398 * 399 * <p>The component's nested components that are available via {@code Component#getComponentsForLifecycle} 400 * are also updated recursively</p> 401 * 402 * @param component - the component whose context is to be updated 403 * @param contextName - a value to be used as a key to retrieve the object 404 * @param contextValue - the value to be placed in the context 405 */ 406 public static void pushObjectToContext(Component component, String contextName, Object contextValue) { 407 if (component == null) { 408 return; 409 } 410 411 component.pushObjectToContext(contextName, contextValue); 412 413 // special container check so we pick up the layout manager 414 if (Container.class.isAssignableFrom(component.getClass())) { 415 LayoutManager layoutManager = ((Container) component).getLayoutManager(); 416 if (layoutManager != null) { 417 // add to layout manager context only if not present 418 if (layoutManager.getContext().get(contextName) != contextValue) { 419 layoutManager.pushObjectToContext(contextName, contextValue); 420 421 for (Component nestedComponent : layoutManager.getComponentsForLifecycle()) { 422 pushObjectToContext(nestedComponent, contextName, contextValue); 423 } 424 } 425 } 426 } 427 428 for (Component nestedComponent : component.getComponentsForLifecycle()) { 429 pushObjectToContext(nestedComponent, contextName, contextValue); 430 } 431 } 432 433 /** 434 * update the contexts of the given components 435 * 436 * <p>calls {@link #updateContextForLine(org.kuali.rice.krad.uif.component.Component, Object, int, String)} 437 * for each component</p> 438 * 439 * @param components - the components whose components to update 440 * @param collectionLine - an instance of the data object for the line 441 * @param lineIndex - the line index 442 * @param lineSuffix id suffix for components in the line to make them unique 443 */ 444 public static void updateContextsForLine(List<? extends Component> components, Object collectionLine, 445 int lineIndex, String lineSuffix) { 446 for (Component component : components) { 447 updateContextForLine(component, collectionLine, lineIndex, lineSuffix); 448 } 449 } 450 451 /** 452 * update the context map for the given component 453 * 454 * <p>The values of {@code UifConstants.ContextVariableNames.LINE} and {@code UifConstants.ContextVariableNames.INDEX} 455 * are set to {@code collectionLine} and {@code lineIndex} respectively.</p> 456 * 457 * @param component - the component whose context is to be updated 458 * @param collectionLine - an instance of the data object for the line 459 * @param lineIndex - the line index 460 * @param lineSuffix id suffix for components in the line to make them unique 461 */ 462 public static void updateContextForLine(Component component, Object collectionLine, int lineIndex, 463 String lineSuffix) { 464 pushObjectToContext(component, UifConstants.ContextVariableNames.LINE, collectionLine); 465 pushObjectToContext(component, UifConstants.ContextVariableNames.INDEX, Integer.valueOf(lineIndex)); 466 pushObjectToContext(component, UifConstants.ContextVariableNames.LINE_SUFFIX, lineSuffix); 467 468 boolean isAddLine = (lineIndex == -1); 469 pushObjectToContext(component, UifConstants.ContextVariableNames.IS_ADD_LINE, isAddLine); 470 } 471 472 /** 473 * Performs sorting logic of the given list of <code>Ordered</code> 474 * instances by its order property 475 * 476 * <p> 477 * Items list is sorted based on its order property. Lower order values are 478 * placed higher in the list. If a item does not have a value assigned for 479 * the order (or is equal to the default order of 0), it will be assigned 480 * the a value based on the given order sequence integer. If two or more 481 * items share the same order value, all but the last item found in the list 482 * will be removed. 483 * </p> 484 * 485 * @param items 486 * @param defaultOrderSequence 487 * @return List<Ordered> sorted items 488 * @see org.kuali.rice.krad.uif.component.Component#getOrder() 489 * @see @see org.springframework.core.Ordered 490 */ 491 public static List<? extends Ordered> sort(List<? extends Ordered> items, int defaultOrderSequence) { 492 List<Ordered> orderedItems = new ArrayList<Ordered>(); 493 494 // do replacement for items with the same order property value 495 Set<Integer> foundOrders = new HashSet<Integer>(); 496 497 // reverse the list, so items later in the list win 498 Collections.reverse(items); 499 for (Ordered component : items) { 500 int order = component.getOrder(); 501 502 // if order not set just add to list 503 if (order == 0) { 504 orderedItems.add(component); 505 } 506 // check if the order value has been used already 507 else if (!foundOrders.contains(Integer.valueOf(order))) { 508 orderedItems.add(component); 509 foundOrders.add(Integer.valueOf(order)); 510 } 511 } 512 513 // now reverse the list back so we can assign defaults for items without 514 // an order value 515 Collections.reverse(items); 516 for (Ordered component : items) { 517 int order = component.getOrder(); 518 519 // if order property not set assign default 520 if (order == 0) { 521 defaultOrderSequence++; 522 while (foundOrders.contains(Integer.valueOf(defaultOrderSequence))) { 523 defaultOrderSequence++; 524 } 525 component.setOrder(defaultOrderSequence); 526 } 527 } 528 529 // now sort the list by its order property 530 Collections.sort(orderedItems, new OrderComparator()); 531 532 return orderedItems; 533 } 534 535 /** 536 * Gets all the input fields contained in this container, but also in 537 * every sub-container that is a child of this container. When called from the top level 538 * View this will be every InputField across all pages. 539 * 540 * @return every InputField that is a child at any level of this container 541 */ 542 public static List<InputField> getAllInputFieldsWithinContainer(Container container) { 543 List<InputField> inputFields = new ArrayList<InputField>(); 544 545 for (Component c : container.getComponentsForLifecycle()) { 546 if (c instanceof InputField) { 547 inputFields.add((InputField) c); 548 } else if (c instanceof Container) { 549 inputFields.addAll(getAllInputFieldsWithinContainer((Container) c)); 550 } else if (c instanceof FieldGroup) { 551 Container cb = ((FieldGroup) c).getGroup(); 552 553 inputFields.addAll(getAllInputFieldsWithinContainer(cb)); 554 } 555 } 556 557 return inputFields; 558 } 559 560 /** 561 * Determines whether the given component contains an expression for the given property name 562 * 563 * @param component component instance to check for expressions 564 * @param propertyName name of the property to determine if there is an expression for 565 * @param collectionMatch if set to true will find an expressions for properties that start with the given 566 * property name (for matching expressions on collections like prop[index] or prop['key']) 567 * @return boolean true if the component has an expression for the property name, false if not 568 */ 569 public static boolean containsPropertyExpression(Component component, String propertyName, 570 boolean collectionMatch) { 571 boolean hasExpression = false; 572 573 Map<String, String> propertyExpressions = component.getPropertyExpressions(); 574 575 if (collectionMatch) { 576 for (String expressionPropertyName : propertyExpressions.keySet()) { 577 if (expressionPropertyName.startsWith(propertyName)) { 578 hasExpression = true; 579 } 580 } 581 } else if (propertyExpressions.containsKey(propertyName)) { 582 hasExpression = true; 583 } 584 585 return hasExpression; 586 } 587 588 }