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    }