View Javadoc
1   /**
2    * Copyright 2005-2015 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 java.util.ArrayList;
19  import java.util.Collection;
20  import java.util.Collections;
21  import java.util.HashMap;
22  import java.util.HashSet;
23  import java.util.LinkedList;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Queue;
27  import java.util.Set;
28  
29  import org.apache.commons.lang.StringUtils;
30  import org.kuali.rice.core.framework.util.ReflectionUtils;
31  import org.kuali.rice.krad.uif.UifConstants;
32  import org.kuali.rice.krad.uif.component.Component;
33  import org.kuali.rice.krad.uif.component.DataBinding;
34  import org.kuali.rice.krad.uif.component.Ordered;
35  import org.kuali.rice.krad.uif.container.CollectionGroup;
36  import org.kuali.rice.krad.uif.container.Container;
37  import org.kuali.rice.krad.uif.container.Group;
38  import org.kuali.rice.krad.uif.field.Field;
39  import org.kuali.rice.krad.uif.field.FieldGroup;
40  import org.kuali.rice.krad.uif.field.InputField;
41  import org.kuali.rice.krad.uif.layout.LayoutManager;
42  import org.kuali.rice.krad.uif.layout.TableLayoutManager;
43  import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle;
44  import org.kuali.rice.krad.uif.lifecycle.ViewLifecycleUtils;
45  import org.kuali.rice.krad.uif.lifecycle.initialize.AssignIdsTask;
46  import org.springframework.core.OrderComparator;
47  
48  /**
49   * ComponentUtils is a utility class providing methods to help create and modify {@link Component} instances.
50   *
51   * @author Kuali Rice Team (rice.collab@kuali.org)
52   */
53  public class ComponentUtils {
54      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(ComponentUtils.class);
55  
56      private ComponentUtils() {}
57  
58      public static <T extends Component> T copy(T component) {
59          return copy(component, null);
60      }
61  
62      public static <T extends Component> T copy(T component, String idSuffix) {
63          if (component == null) {
64              return null;
65          }
66  
67          T copy = CopyUtils.copy(component);
68  
69          if (StringUtils.isNotBlank(idSuffix)) {
70              updateIdsWithSuffixNested(copy, idSuffix);
71          }
72  
73          return copy;
74      }
75  
76      /**
77       * Copy a list of components
78       *
79       * @param <T> component type
80       * @param <T> component type
81       * @param components the list of components to copy
82       * @return the copied list
83       */
84      public static <T extends Component> List<T> copy(List<T> components) {
85          if (components != null) {
86              List<T> componentsCopy = new ArrayList<T>();
87              for (T component : components) {
88                  T copiedComponent = copy(component);
89                  componentsCopy.add(copiedComponent);
90              }
91  
92              return componentsCopy;
93          }
94  
95          return new ArrayList<T>();
96      }
97  
98      /**
99       * Adjusts the ids to contain the given suffix and adds the giving binding prefix for the list of fields.
100      *
101      * @param <T> component type
102      * @param fields list of fields to bind and id
103      * @param addBindingPrefix prefix to add to the binding path
104      * @param idSuffix id suffix
105      */
106     public static <T extends Field> void bindAndIdFieldList(List<T> fields, String addBindingPrefix, String idSuffix) {
107         updateIdsWithSuffixNested(fields, idSuffix);
108         prefixBindingPath(fields, addBindingPrefix);
109     }
110 
111     public static <T extends Component> T copyComponent(T component, String addBindingPrefix, String idSuffix) {
112         T copy = copy(component, idSuffix);
113 
114         prefixBindingPathNested(copy, addBindingPrefix);
115 
116         return copy;
117     }
118 
119     public static <T extends Component> List<T> copyComponentList(List<T> components, String idSuffix) {
120         if (components == null || components.isEmpty()) {
121             return Collections.emptyList();
122         }
123 
124         List<T> copiedComponentList = new ArrayList<T>(components.size());
125 
126         for (T field : components) {
127             T copiedComponent = copy(field, idSuffix);
128             copiedComponentList.add(copiedComponent);
129         }
130 
131         return copiedComponentList;
132     }
133 
134     public static <T extends Object> List<T> getComponentsOfType(List<? extends Component> items,
135             Class<T> componentType) {
136         if (items == null || items.isEmpty()) {
137             return Collections.emptyList();
138         }
139 
140         List<T> typeComponents = Collections.emptyList();
141 
142         for (Component component : items) {
143 
144             if (!componentType.isInstance(component)) {
145                 continue;
146             }
147 
148             if (typeComponents.isEmpty()) {
149                 typeComponents = new ArrayList<T>(items.size());
150             }
151 
152             typeComponents.add(componentType.cast(component));
153         }
154 
155         return typeComponents;
156     }
157 
158     /**
159      * Gets all components of the give type that are within the items list of the container, or within a nested
160      * container or field group.
161      *
162      * @param container container instance to pull components from
163      * @param componentType type for components to pull
164      * @param <T> type for component to pull
165      * @return List of nested components with the given type
166      */
167     public static <T extends Component> List<T> getNestedContainerComponents(Container container,
168             Class<T> componentType) {
169         List<T> typeComponents = new ArrayList<T>();
170 
171         if (container == null) {
172             return typeComponents;
173         }
174 
175         for (Component item : container.getItems()) {
176             if (item == null) {
177                 continue;
178             }
179 
180             if (item instanceof Container) {
181                 typeComponents.addAll(getNestedContainerComponents((Container) item, componentType));
182             } else if (item instanceof FieldGroup) {
183                 typeComponents.addAll(getNestedContainerComponents(((FieldGroup) item).getGroup(), componentType));
184             } else if (componentType.isAssignableFrom(item.getClass())) {
185                 typeComponents.add(componentType.cast(item));
186             }
187         }
188 
189         return typeComponents;
190     }
191 
192     public static <T extends Component> List<T> getNestedNonCollectionComponents(List<Component> items,
193             Class<T> componentType) {
194         List<T> typeComponents = new ArrayList<T>();
195 
196         if (items == null) {
197             return typeComponents;
198         }
199 
200         for (Component item : items) {
201             if (item == null) {
202                 continue;
203             }
204 
205             if (item instanceof Container && !(item instanceof CollectionGroup)) {
206                 typeComponents.addAll(getNestedNonCollectionComponents((Container) item, componentType));
207             } else if (item instanceof FieldGroup) {
208                 typeComponents.addAll(getNestedNonCollectionComponents(((FieldGroup) item).getGroup(), componentType));
209             } else if (componentType.isAssignableFrom(item.getClass())) {
210                 typeComponents.add(componentType.cast(item));
211             }
212         }
213 
214         return typeComponents;
215     }
216 
217     public static <T extends Component> List<T> getNestedNonCollectionComponents(Container container,
218             Class<T> componentType) {
219         List<T> typeComponents = new ArrayList<T>();
220 
221         if (container == null) {
222             return typeComponents;
223         }
224 
225         for (Component item : container.getItems()) {
226             if (item == null) {
227                 continue;
228             }
229 
230             if (item instanceof Container && !(item instanceof CollectionGroup)) {
231                 typeComponents.addAll(getNestedNonCollectionComponents((Container) item, componentType));
232             } else if (item instanceof FieldGroup) {
233                 typeComponents.addAll(getNestedNonCollectionComponents(((FieldGroup) item).getGroup(), componentType));
234             } else if (componentType.isAssignableFrom(item.getClass())) {
235                 typeComponents.add(componentType.cast(item));
236             }
237         }
238 
239         return typeComponents;
240     }
241 
242     /**
243      * Get all nested children of a given component.
244      *
245      * @param component The component to search.
246      * @return All nested children of the component.
247      * @see ViewLifecycleUtils#getElementsForLifecycle(LifecycleElement)
248      */
249     public static List<Component> getAllNestedComponents(Component component) {
250         if (component == null) {
251             return Collections.emptyList();
252         }
253 
254         List<Component> components = Collections.emptyList();
255         @SuppressWarnings("unchecked") Queue<LifecycleElement> elementQueue = RecycleUtils.getInstance(
256                 LinkedList.class);
257         elementQueue.offer(component);
258 
259         try {
260             while (!elementQueue.isEmpty()) {
261                 LifecycleElement currentElement = elementQueue.poll();
262 
263                 if (currentElement == null) {
264                     continue;
265                 }
266 
267                 if (currentElement instanceof Component && currentElement != component) {
268                     if (components.isEmpty()) {
269                         components = new ArrayList<Component>();
270                     }
271 
272                     components.add((Component) currentElement);
273                 }
274 
275                 elementQueue.addAll(ViewLifecycleUtils.getElementsForLifecycle(currentElement).values());
276             }
277         } finally {
278             elementQueue.clear();
279             RecycleUtils.recycle(elementQueue);
280         }
281 
282         return components;
283     }
284 
285     /**
286      * Searches for the component with the given id within the given list of components
287      *
288      * @param components list of components to search through
289      * @param componentId id for the component to find
290      * @return component found in the list or null
291      */
292     public static Component findComponentInList(List<Component> components, String componentId) {
293         for (Component component : components) {
294             if (component != null && StringUtils.equals(component.getId(), componentId)) {
295                 return component;
296             }
297         }
298 
299         return null;
300     }
301 
302     public static void prefixBindingPath(List<? extends Component> components, String addBindingPrefix) {
303         for (Component component : components) {
304             prefixBindingPath(component, addBindingPrefix);
305         }
306     }
307 
308     public static void prefixBindingPath(Component component, String addBindingPrefix) {
309         if (component instanceof DataBinding) {
310             prefixBindingPath((DataBinding) component, addBindingPrefix);
311         } else if ((component instanceof FieldGroup) && (((FieldGroup) component).getItems() != null)) {
312             List<? extends Component> fieldGroupItems = ((FieldGroup) component).getItems();
313             prefixBindingPath(fieldGroupItems, addBindingPrefix);
314         } else if ((component instanceof Group) && (((Group) component).getItems() != null) &&
315                 !(component instanceof CollectionGroup)) {
316             List<? extends Component> groupItems = ((Group) component).getItems();
317             prefixBindingPath(groupItems, addBindingPrefix);
318         }
319     }
320 
321     public static void prefixBindingPath(DataBinding field, String addBindingPrefix) {
322         String bindingPrefix = addBindingPrefix;
323         if (StringUtils.isNotBlank(field.getBindingInfo().getBindByNamePrefix())) {
324             bindingPrefix += "." + field.getBindingInfo().getBindByNamePrefix();
325         }
326         field.getBindingInfo().setBindByNamePrefix(bindingPrefix);
327     }
328 
329     public static void prefixBindingPathNested(Component component, String addBindingPrefix) {
330         @SuppressWarnings("unchecked") Queue<LifecycleElement> elementQueue = RecycleUtils.getInstance(
331                 LinkedList.class);
332         elementQueue.offer(component);
333 
334         try {
335             while (!elementQueue.isEmpty()) {
336                 LifecycleElement currentElement = elementQueue.poll();
337                 if (currentElement == null) {
338                     continue;
339                 }
340 
341                 if (currentElement instanceof DataBinding) {
342                     if (LOG.isDebugEnabled()) {
343                         LOG.info("setting nested binding prefix '" + addBindingPrefix + "' on " + currentElement);
344                     }
345                     prefixBindingPath((DataBinding) currentElement, addBindingPrefix);
346                 }
347 
348                 elementQueue.addAll(ViewLifecycleUtils.getElementsForLifecycle(currentElement).values());
349             }
350         } finally {
351             elementQueue.clear();
352             RecycleUtils.recycle(elementQueue);
353         }
354     }
355 
356     public static void updateIdsWithSuffixNested(List<? extends Component> components, String idSuffix) {
357         for (Component component : components) {
358             updateIdsWithSuffixNested(component, idSuffix);
359         }
360     }
361 
362     public static void updateIdsWithSuffixNested(Component component, String idSuffix) {
363         updateIdWithSuffix(component, idSuffix);
364 
365         updateChildIdsWithSuffixNested(component, idSuffix);
366     }
367 
368     /**
369      * add a suffix to the id
370      *
371      * @param element the component instance whose id will be changed
372      * @param idSuffix the suffix to be appended
373      */
374     public static void updateIdWithSuffix(LifecycleElement element, String idSuffix) {
375         if (StringUtils.isBlank(idSuffix)) {
376             return;
377         }
378 
379         if (element != null) {
380             element.setId(element.getId() + idSuffix);
381         }
382 
383         if (element instanceof Container) {
384             LayoutManager manager = ((Container) element).getLayoutManager();
385 
386             if (manager != null) {
387                 manager.setId(manager.getId() + idSuffix);
388             }
389         }
390     }
391 
392     public static void updateChildIdsWithSuffixNested(Component component, String idSuffix) {
393         @SuppressWarnings("unchecked") Queue<LifecycleElement> elementQueue = RecycleUtils.getInstance(
394                 LinkedList.class);
395         try {
396             elementQueue.addAll(ViewLifecycleUtils.getElementsForLifecycle(component).values());
397 
398             while (!elementQueue.isEmpty()) {
399                 LifecycleElement currentElement = elementQueue.poll();
400                 if (currentElement == null) {
401                     continue;
402                 }
403 
404                 if (currentElement instanceof Component) {
405                     updateIdWithSuffix((Component) currentElement, idSuffix);
406                     elementQueue.addAll(((Component) currentElement).getPropertyReplacerComponents());
407                 }
408 
409                 elementQueue.addAll(ViewLifecycleUtils.getElementsForLifecycle(currentElement).values());
410             }
411         } finally {
412             elementQueue.clear();
413             RecycleUtils.recycle(elementQueue);
414         }
415     }
416 
417     /**
418      * Generate a hash code unique within the current view for a single lifecycle element. A unique
419      * ID value will be assigned to the lifecycle element, replacing the current ID.
420      *
421      * <p>This method may only be called during the view lifecycle.</p>
422      *
423      * @param element The element to generate a hash code for.
424      * @param seed A hash value to use as a seed for the new hash.
425      * @return A hash code based on the provided element and seed value.
426      * @see AssignIdsTask For a complete description of the algorithm. This method implements a
427      * single step in the algorithm described in that task.
428      */
429     public static int generateId(LifecycleElement element, int seed) {
430         if (element == null) {
431             return seed;
432         }
433 
434         final int prime = 6971;
435         int hash = prime * seed + element.getClass().getName().hashCode();
436 
437         String id = element.getId();
438         hash *= prime;
439         if (id != null) {
440             hash += id.hashCode();
441         }
442 
443         do {
444             hash *= 4507;
445             id = Long.toString(((long) hash) - ((long) Integer.MIN_VALUE), 36);
446         } while (!ViewLifecycle.getView().getViewIndex().observeAssignedId(id));
447 
448         element.setId(UifConstants.COMPONENT_ID_PREFIX + id);
449 
450         return hash;
451     }
452 
453     /**
454      * Replace all IDs from a component and its children with new generated ID values.
455      *
456      * <p>If there are features that depend on a static id of this
457      * component, this call may cause errors.</p>
458      *
459      * @param components A list of component to clear all IDs from.
460      * @see AssignIdsTask For a complete description of the algorithm.
461      */
462     public static void clearAndAssignIds(List<? extends Component> components) {
463         if (components == null || components.isEmpty()) {
464             return;
465         }
466 
467         int hash = 1;
468         @SuppressWarnings("unchecked") Queue<LifecycleElement> toClear = RecycleUtils.getInstance(LinkedList.class);
469         toClear.addAll(components);
470         try {
471             while (!toClear.isEmpty()) {
472                 LifecycleElement element = toClear.poll();
473 
474                 hash = generateId(element, hash);
475 
476                 for (LifecycleElement nested : ViewLifecycleUtils.getElementsForLifecycle(element).values()) {
477                     if (nested != null) {
478                         toClear.add(nested);
479                     }
480                 }
481 
482                 if (element instanceof Component) {
483                     List<Component> propertyReplacerComponents = ((Component) element).getPropertyReplacerComponents();
484                     if (propertyReplacerComponents == null) {
485                         continue;
486                     }
487 
488                     for (Component nested : propertyReplacerComponents) {
489                         if (nested != null) {
490                             toClear.add(nested);
491                         }
492                     }
493                 }
494             }
495         } finally {
496             toClear.clear();
497             RecycleUtils.recycle(toClear);
498         }
499     }
500 
501     /**
502      * Traverse a component tree, setting a property on all components for which the property is writable.
503      *
504      * @param <T> component type
505      * @param <T> component type
506      * @param components The components to traverse.
507      * @param propertyPath The property path to set.
508      * @param propertyValue The property value to set.
509      * @see ObjectPropertyUtils#isWritableProperty(Object, String)
510      * @see ObjectPropertyUtils#setPropertyValue(Object, String, Object)
511      */
512     public static <T extends Component> void setComponentsPropertyDeep(List<T> components, String propertyPath,
513             Object propertyValue) {
514         if (components == null || components.isEmpty()) {
515             return;
516         }
517 
518         Set<Class<?>> skipTypes = null;
519         @SuppressWarnings("unchecked") Queue<LifecycleElement> elementQueue = RecycleUtils.getInstance(
520                 LinkedList.class);
521         elementQueue.addAll(components);
522 
523         try {
524             while (!elementQueue.isEmpty()) {
525                 LifecycleElement currentElement = elementQueue.poll();
526                 if (currentElement == null) {
527                     continue;
528                 }
529 
530                 elementQueue.addAll(ViewLifecycleUtils.getElementsForLifecycle(currentElement).values());
531 
532                 Class<?> componentClass = currentElement.getClass();
533                 if (skipTypes != null && skipTypes.contains(componentClass)) {
534                     continue;
535                 }
536 
537                 if (!ObjectPropertyUtils.isWritableProperty(currentElement, propertyPath)) {
538                     if (skipTypes == null) {
539                         skipTypes = new HashSet<Class<?>>();
540                     }
541                     skipTypes.add(componentClass);
542                     continue;
543                 }
544 
545                 ObjectPropertyUtils.setPropertyValue(currentElement, propertyPath, propertyValue, true);
546             }
547         } finally {
548             elementQueue.clear();
549             RecycleUtils.recycle(elementQueue);
550         }
551     }
552 
553     /**
554      * Traverse a component tree, setting a property on all components for which the property is writable.
555      *
556      * @param component The component to traverse.
557      * @param propertyPath The property path to set.
558      * @param propertyValue The property value to set.
559      * @see ObjectPropertyUtils#isWritableProperty(Object, String)
560      * @see ObjectPropertyUtils#setPropertyValue(Object, String, Object)
561      */
562     public static void setComponentPropertyDeep(Component component, String propertyPath, Object propertyValue) {
563         setComponentsPropertyDeep(Collections.singletonList(component), propertyPath, propertyValue);
564     }
565 
566     /**
567      * Sets a property on the given component and removes any expressions for that property so the value is not
568      * overridden
569      *
570      * @param component component instance to set property on
571      * @param propertyName name of property to set
572      * @param propertyValue value to set property to
573      */
574     public static void setComponentPropertyFinal(Component component, String propertyName, Object propertyValue) {
575         if (component == null) {
576             return;
577         }
578 
579         ObjectPropertyUtils.setPropertyValue(component, propertyName, propertyValue);
580 
581         if ((component.getPropertyExpressions() != null) && component.getPropertyExpressions().containsKey(
582                 propertyName)) {
583             component.getPropertyExpressions().remove(propertyName);
584         }
585     }
586 
587     /**
588      * Indicates if the given component has configuration that it allows it to be refreshed.
589      *
590      * @param component instance to check
591      * @return true if component can be refreshed, false if not
592      */
593     public static boolean canBeRefreshed(Component component) {
594         boolean hasRefreshCondition = StringUtils.isNotBlank(component.getProgressiveRender()) ||
595                 StringUtils.isNotBlank(component.getConditionalRefresh()) || (component.getRefreshTimer() > 0) ||
596                 (component.getRefreshWhenChangedPropertyNames() != null && !component
597                         .getRefreshWhenChangedPropertyNames().isEmpty());
598 
599         boolean isInlineEditField =
600                 component instanceof InputField && (((InputField) component).isInlineEdit() || ((InputField) component)
601                         .isAjaxInlineEdit());
602 
603         return hasRefreshCondition || component.isRefreshedByAction() || component.isDisclosedByAction() ||
604                 component.isRetrieveViaAjax() || isInlineEditField;
605     }
606 
607     /**
608      * places a key, value pair in each context map of a list of components
609      *
610      * @param elements the list of elements
611      * @param contextName a value to be used as a key to retrieve the object
612      * @param contextValue the value to be placed in the context
613      */
614     public static void pushObjectToContext(Collection<? extends LifecycleElement> elements, String contextName,
615             Object contextValue) {
616         if (elements == null || elements.isEmpty()) {
617             return;
618         }
619 
620         Queue<LifecycleElement> elementQueue = new LinkedList<LifecycleElement>();
621 
622         try {
623             elementQueue.addAll(elements);
624             while (!elementQueue.isEmpty()) {
625                 LifecycleElement currentElement = elementQueue.poll();
626 
627                 if (currentElement == null) {
628                     continue;
629                 }
630 
631                 if (currentElement instanceof Component) {
632                     ((Component) currentElement).pushObjectToContext(contextName, contextValue);
633                 }
634 
635                 elementQueue.addAll(ViewLifecycleUtils.getElementsForLifecycle(currentElement).values());
636             }
637         } finally {
638             elementQueue.clear();
639             RecycleUtils.recycle(elementQueue);
640         }
641     }
642 
643     /**
644      * pushes object to a component's context so that it is available from {@link Component#getContext()}
645      *
646      * <p>The component's nested components that are available via {@code Component#getComponentsForLifecycle}
647      * are also updated recursively</p>
648      *
649      * @param component the component whose context is to be updated
650      * @param contextName a value to be used as a key to retrieve the object
651      * @param contextValue the value to be placed in the context
652      */
653     public static void pushObjectToContext(Component component, String contextName, Object contextValue) {
654         if (component == null) {
655             return;
656         }
657 
658         pushObjectToContext(Collections.singletonList(component), contextName, contextValue);
659     }
660 
661     /**
662      * places a all entries from a map into each context map of a list of components
663      *
664      * @param components The list components.
665      * @param sourceContext The source context map.
666      */
667     public static void pushAllToContext(List<? extends Component> components, Map<String, Object> sourceContext) {
668         if (components == null || components.isEmpty()) {
669             return;
670         }
671 
672         @SuppressWarnings("unchecked") Queue<LifecycleElement> elementQueue = RecycleUtils.getInstance(
673                 LinkedList.class);
674         try {
675             elementQueue.addAll(components);
676             while (!elementQueue.isEmpty()) {
677                 LifecycleElement currentElement = elementQueue.poll();
678 
679                 if (currentElement == null) {
680                     continue;
681                 }
682 
683                 if (currentElement instanceof Component) {
684                     ((Component) currentElement).pushAllToContext(sourceContext);
685                 }
686 
687                 elementQueue.addAll(ViewLifecycleUtils.getElementsForLifecycle(currentElement).values());
688             }
689         } finally {
690             elementQueue.clear();
691             RecycleUtils.recycle(elementQueue);
692         }
693     }
694 
695     /**
696      * pushes object to a component's context so that it is available from {@link Component#getContext()}
697      *
698      * <p>The component's nested components that are available via {@code Component#getComponentsForLifecycle}
699      * are also updated recursively</p>
700      *
701      * @param component the component whose context is to be updated
702      * @param sourceContext The source context map.
703      */
704     public static void pushAllToContext(Component component, Map<String, Object> sourceContext) {
705         if (component == null) {
706             return;
707         }
708 
709         pushAllToContext(Collections.singletonList(component), sourceContext);
710     }
711 
712     /**
713      * Update the contexts of the given components.
714      *
715      * <p>Calls {@link ComponentUtils#updateContextForLine} for each component</p>
716      *
717      * @param components the components whose components to update
718      * @param collectionGroup collection group the components are associated with
719      * @param collectionLine an instance of the data object for the line
720      * @param lineIndex the line index
721      * @param lineSuffix id suffix for components in the line to make them unique
722      */
723     public static void updateContextsForLine(List<? extends Component> components, CollectionGroup collectionGroup,
724             Object collectionLine, int lineIndex, String lineSuffix) {
725         for (Component component : components) {
726             updateContextForLine(component, collectionGroup, collectionLine, lineIndex, lineSuffix);
727         }
728     }
729 
730     /**
731      * update the context map for the given component
732      *
733      * <p>The values of {@code UifConstants.ContextVariableNames.LINE} and {@code UifConstants.ContextVariableNames.INDEX}
734      * are set to {@code collectionLine} and {@code lineIndex} respectively.</p>
735      *
736      * @param component the component whose context is to be updated
737      * @param collectionGroup collection group the component is associated with
738      * @param collectionLine an instance of the data object for the line
739      * @param lineIndex the line index
740      * @param lineSuffix id suffix for components in the line, not if the collection group has a container id suffix
741      * it will be appended to the lineSuffix for the final exported context entry
742      */
743     public static void updateContextForLine(Component component, CollectionGroup collectionGroup, Object collectionLine,
744             int lineIndex, String lineSuffix) {
745         // line id suffix that we export as expression variable will contain the container suffix as well
746         // so correct references to ids for line components can be configured in the XML
747         if (StringUtils.isNotBlank(collectionGroup.getContainerIdSuffix())) {
748             lineSuffix = lineSuffix + collectionGroup.getContainerIdSuffix();
749         }
750 
751         Map<String, Object> toUpdate = new HashMap<String, Object>(5);
752         toUpdate.put(UifConstants.ContextVariableNames.COLLECTION_GROUP, collectionGroup);
753         toUpdate.put(UifConstants.ContextVariableNames.LINE, collectionLine);
754         toUpdate.put(UifConstants.ContextVariableNames.INDEX, Integer.valueOf(lineIndex));
755         toUpdate.put(UifConstants.ContextVariableNames.LINE_SUFFIX, lineSuffix);
756 
757         boolean isAddLine = (lineIndex == -1);
758         toUpdate.put(UifConstants.ContextVariableNames.IS_ADD_LINE, isAddLine);
759         pushAllToContext(component, toUpdate);
760     }
761 
762     /**
763      * Sets the context of the given lifecycle element to null, then using reflection recursively finds any
764      * lifecycle element children and sets their context to null.
765      *
766      * @param lifecycleElement lifecycle element instance to clean
767      */
768     public static void cleanContextDeap(LifecycleElement lifecycleElement) {
769         if (lifecycleElement == null) {
770             return;
771         }
772 
773         lifecycleElement.setContext(null);
774 
775         // find any children that are lifecycle elements and clean them as well
776         Class<?> elementClass = lifecycleElement.getClass();
777 
778         List<java.lang.reflect.Field> fields = ReflectionUtils.getAllFields(elementClass);
779         for (java.lang.reflect.Field field : fields) {
780             // Check for lists that can contain lifecycle elements
781             if (Collection.class.isAssignableFrom(field.getType())) {
782                 ReflectionUtils.makeAccessible(field);
783                 Collection<Object> elements = (Collection<Object>) ReflectionUtils.getField(field, lifecycleElement);
784                 if (elements != null) {
785                     for (Object element : elements) {
786                         if (element != null && LifecycleElement.class.isAssignableFrom(element.getClass())) {
787                             cleanContextDeap((LifecycleElement) element);
788                         }
789                     }
790                 }
791                 // Check for Maps that can contain lifecycle elements
792             } else if (Map.class.isAssignableFrom(field.getType())) {
793                 ReflectionUtils.makeAccessible(field);
794                 Map<Object, Object> elements = (Map<Object, Object>) ReflectionUtils.getField(field, lifecycleElement);
795                 if (elements != null) {
796                     for (Object element : elements.entrySet()) {
797                         if (element != null && LifecycleElement.class.isAssignableFrom(element.getClass())) {
798                             cleanContextDeap((LifecycleElement) element);
799                         }
800                     }
801                 }
802                 // Check if field is a lifecycle element itself
803             } else if (LifecycleElement.class.isAssignableFrom(field.getType())) {
804                 ReflectionUtils.makeAccessible(field);
805                 LifecycleElement nestedElement = (LifecycleElement) ReflectionUtils.getField(field, lifecycleElement);
806 
807                 cleanContextDeap(nestedElement);
808             }
809         }
810     }
811 
812     /**
813      * Performs sorting logic of the given list of <code>Ordered</code>
814      * instances by its order property
815      *
816      * <p>
817      * Items list is sorted based on its order property. Lower order values are
818      * placed higher in the list. If a item does not have a value assigned for
819      * the order (or is equal to the default order of 0), it will be assigned
820      * the a value based on the given order sequence integer. If two or more
821      * items share the same order value, all but the last item found in the list
822      * will be removed.
823      * </p>
824      *
825      * @param <T> ordered type
826      * @param <T> ordered type
827      * @param items
828      * @param defaultOrderSequence
829      * @return List<Ordered> sorted items
830      * @see org.kuali.rice.krad.uif.component.Component#getOrder()
831      * @see org.springframework.core.Ordered
832      */
833     public static <T extends Ordered> List<T> sort(List<T> items, int defaultOrderSequence) {
834         if (items == null) {
835             return null;
836         }
837 
838         List<T> orderedItems = new ArrayList<T>(items.size());
839 
840         // do replacement for items with the same order property value
841         Set<Integer> foundOrders = new HashSet<Integer>();
842 
843         // reverse the list, so items later in the list win
844         for (int i = items.size() - 1; i >= 0; i--) {
845             T component = items.get(i);
846             int order = component.getOrder();
847 
848             // if order not set just add to list
849             if (order == 0) {
850                 orderedItems.add(component);
851             }
852             // check if the order value has been used already
853             else if (!foundOrders.contains(Integer.valueOf(order))) {
854                 orderedItems.add(component);
855                 foundOrders.add(Integer.valueOf(order));
856             }
857         }
858 
859         // now reverse the list back so we can assign defaults for items without
860         // an order value
861         for (int i = 0; i < items.size(); i++) {
862             Ordered component = items.get(i);
863             int order = component.getOrder();
864 
865             // if order property not set assign default
866             if (order == 0) {
867                 defaultOrderSequence++;
868                 while (foundOrders.contains(Integer.valueOf(defaultOrderSequence))) {
869                     defaultOrderSequence++;
870                 }
871                 component.setOrder(defaultOrderSequence);
872             }
873         }
874 
875         // now sort the list by its order property
876         Collections.sort(orderedItems, new OrderComparator());
877 
878         return orderedItems;
879     }
880 
881     /**
882      * Gets all the input fields contained in this container, but also in
883      * every sub-container that is a child of this container.  When called from the top level
884      * View this will be every InputField across all pages.
885      *
886      * @param container container to scan for input fields
887      * @return every InputField that is a child at any level of this container
888      */
889     public static List<InputField> getAllInputFieldsWithinContainer(Container container) {
890         List<InputField> inputFields = new ArrayList<InputField>();
891 
892         for (LifecycleElement c : ViewLifecycleUtils.getElementsForLifecycle(container).values()) {
893             if (c instanceof InputField) {
894                 inputFields.add((InputField) c);
895             } else if (c instanceof Container) {
896                 inputFields.addAll(getAllInputFieldsWithinContainer((Container) c));
897             } else if (c instanceof FieldGroup) {
898                 Container cb = ((FieldGroup) c).getGroup();
899 
900                 inputFields.addAll(getAllInputFieldsWithinContainer(cb));
901             }
902         }
903 
904         return inputFields;
905     }
906 
907     /**
908      * Determines whether the given component contains an expression for the given property name
909      *
910      * @param component component instance to check for expressions
911      * @param propertyName name of the property to determine if there is an expression for
912      * @param collectionMatch if set to true will find an expressions for properties that start with the given
913      * property name (for matching expressions on collections like prop[index] or prop['key'])
914      * @return true if the component has an expression for the property name, false if not
915      */
916     public static boolean containsPropertyExpression(Component component, String propertyName,
917             boolean collectionMatch) {
918         boolean hasExpression = false;
919 
920         Map<String, String> propertyExpressions = component.getPropertyExpressions();
921 
922         if (collectionMatch) {
923             for (String expressionPropertyName : propertyExpressions.keySet()) {
924                 if (expressionPropertyName.startsWith(propertyName)) {
925                     hasExpression = true;
926                 }
927             }
928         } else if (propertyExpressions.containsKey(propertyName)) {
929             hasExpression = true;
930         }
931 
932         return hasExpression;
933     }
934 
935     /**
936      * Adjust nestingLevel properties for collections which use RichTable with forceLocalJsonData on and for all of its
937      * potentially additional nested subcollections
938      *
939      * @param container container to traverse and update nested levels in
940      * @param currentLevel the current nesting level, the initial call to this method should be 0
941      */
942     public static void adjustNestedLevelsForTableCollections(Container container, int currentLevel) {
943         if (container != null
944                 && container instanceof CollectionGroup
945                 && container.getLayoutManager() != null
946                 && container.getLayoutManager() instanceof TableLayoutManager
947                 && ((TableLayoutManager) container.getLayoutManager()).getRichTable() != null
948                 && ((TableLayoutManager) container.getLayoutManager()).getRichTable().isRender()
949                 && ((TableLayoutManager) container.getLayoutManager()).getRichTable().isForceLocalJsonData()) {
950             ((TableLayoutManager) container.getLayoutManager()).getRichTable().setNestedLevel(currentLevel);
951             currentLevel++;
952         }
953 
954         if (container != null) {
955             List<Container> subContainers = ViewLifecycleUtils.getNestedElementsOfTypeShallow(container,
956                     Container.class);
957             for (Container subContainer : subContainers) {
958                 adjustNestedLevelsForTableCollections(subContainer, currentLevel);
959             }
960 
961             List<FieldGroup> subFieldGroups = ViewLifecycleUtils.getNestedElementsOfTypeShallow(container,
962                     FieldGroup.class);
963             for (FieldGroup fieldGroup : subFieldGroups) {
964                 if (fieldGroup != null) {
965                     adjustNestedLevelsForTableCollections(fieldGroup.getGroup(), currentLevel);
966                 }
967             }
968         }
969     }
970 
971 }