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 }