001/**
002 * Copyright 2005-2015 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 */
016package org.kuali.rice.krad.uif.util;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.LinkedList;
024import java.util.List;
025import java.util.Map;
026import java.util.Queue;
027import java.util.Set;
028
029import org.apache.commons.lang.StringUtils;
030import org.kuali.rice.core.framework.util.ReflectionUtils;
031import org.kuali.rice.krad.uif.UifConstants;
032import org.kuali.rice.krad.uif.component.Component;
033import org.kuali.rice.krad.uif.component.DataBinding;
034import org.kuali.rice.krad.uif.component.Ordered;
035import org.kuali.rice.krad.uif.container.CollectionGroup;
036import org.kuali.rice.krad.uif.container.Container;
037import org.kuali.rice.krad.uif.container.Group;
038import org.kuali.rice.krad.uif.field.Field;
039import org.kuali.rice.krad.uif.field.FieldGroup;
040import org.kuali.rice.krad.uif.field.InputField;
041import org.kuali.rice.krad.uif.layout.LayoutManager;
042import org.kuali.rice.krad.uif.layout.TableLayoutManager;
043import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle;
044import org.kuali.rice.krad.uif.lifecycle.ViewLifecycleUtils;
045import org.kuali.rice.krad.uif.lifecycle.initialize.AssignIdsTask;
046import org.springframework.core.OrderComparator;
047
048/**
049 * ComponentUtils is a utility class providing methods to help create and modify {@link Component} instances.
050 *
051 * @author Kuali Rice Team (rice.collab@kuali.org)
052 */
053public class ComponentUtils {
054    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(ComponentUtils.class);
055
056    private ComponentUtils() {}
057
058    public static <T extends Component> T copy(T component) {
059        return copy(component, null);
060    }
061
062    public static <T extends Component> T copy(T component, String idSuffix) {
063        if (component == null) {
064            return null;
065        }
066
067        T copy = CopyUtils.copy(component);
068
069        if (StringUtils.isNotBlank(idSuffix)) {
070            updateIdsWithSuffixNested(copy, idSuffix);
071        }
072
073        return copy;
074    }
075
076    /**
077     * Copy a list of components
078     *
079     * @param <T> component type
080     * @param <T> component type
081     * @param components the list of components to copy
082     * @return the copied list
083     */
084    public static <T extends Component> List<T> copy(List<T> components) {
085        if (components != null) {
086            List<T> componentsCopy = new ArrayList<T>();
087            for (T component : components) {
088                T copiedComponent = copy(component);
089                componentsCopy.add(copiedComponent);
090            }
091
092            return componentsCopy;
093        }
094
095        return new ArrayList<T>();
096    }
097
098    /**
099     * 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}