001    /**
002     * Copyright 2005-2011 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.container;
017    
018    import java.util.ArrayList;
019    import java.util.List;
020    
021    import org.apache.commons.lang.StringUtils;
022    import org.kuali.rice.krad.uif.UifConstants;
023    import org.kuali.rice.krad.uif.UifParameters;
024    import org.kuali.rice.krad.uif.component.BindingInfo;
025    import org.kuali.rice.krad.uif.component.Component;
026    import org.kuali.rice.krad.uif.component.DataBinding;
027    import org.kuali.rice.krad.uif.field.ActionField;
028    import org.kuali.rice.krad.uif.field.DataField;
029    import org.kuali.rice.krad.uif.field.Field;
030    import org.kuali.rice.krad.uif.field.LabelField;
031    import org.kuali.rice.krad.uif.util.ComponentUtils;
032    import org.kuali.rice.krad.uif.view.View;
033    import org.kuali.rice.krad.uif.widget.QuickFinder;
034    
035    /**
036     * Group that holds a collection of objects and configuration for presenting the
037     * collection in the UI. Supports functionality such as add line, line actions,
038     * and nested collections.
039     *
040     * <p>
041     * Note the standard header/footer can be used to give a header to the
042     * collection as a whole, or to provide actions that apply to the entire
043     * collection
044     * </p>
045     *
046     * <p>
047     * For binding purposes the binding path of each row field is indexed. The name
048     * property inherited from <code>ComponentBase</code> is used as the collection
049     * name. The collectionObjectClass property is used to lookup attributes from
050     * the data dictionary.
051     * </p>
052     *
053     * @author Kuali Rice Team (rice.collab@kuali.org)
054     */
055    public class CollectionGroup extends Group implements DataBinding {
056        private static final long serialVersionUID = -6496712566071542452L;
057    
058        private Class<?> collectionObjectClass;
059    
060        private String propertyName;
061        private BindingInfo bindingInfo;
062    
063        private boolean renderAddLine;
064        private String addLinePropertyName;
065        private BindingInfo addLineBindingInfo;
066        private LabelField addLineLabelField;
067        private List<? extends Component> addLineFields;
068        private List<ActionField> addLineActionFields;
069    
070        private boolean renderLineActions;
071        private List<ActionField> actionFields;
072    
073        private boolean renderSelectField;
074        private String selectPropertyName;
075    
076        private QuickFinder collectionLookup;
077    
078        private boolean showHideInactiveButton;
079        private boolean showInactive;
080        private CollectionFilter activeCollectionFilter;
081        private List<CollectionFilter> filters;
082    
083        private List<CollectionGroup> subCollections;
084        private String subCollectionSuffix;
085    
086        private CollectionGroupBuilder collectionGroupBuilder;
087        private CollectionGroupSecurity collectionGroupSecurity;
088    
089        public CollectionGroup() {
090            renderAddLine = true;
091            renderLineActions = true;
092            showInactive = false;
093            showHideInactiveButton = true;
094            renderSelectField = false;
095    
096            collectionGroupSecurity = new CollectionGroupSecurity();
097    
098            filters = new ArrayList<CollectionFilter>();
099            actionFields = new ArrayList<ActionField>();
100            addLineFields = new ArrayList<Field>();
101            addLineActionFields = new ArrayList<ActionField>();
102            subCollections = new ArrayList<CollectionGroup>();
103        }
104    
105        /**
106         * The following actions are performed:
107         *
108         * <ul>
109         * <li>Set fieldBindModelPath to the collection model path (since the fields
110         * have to belong to the same model as the collection)</li>
111         * <li>Set defaults for binding</li>
112         * <li>Default add line field list to groups items list</li>
113         * <li>Sets default active collection filter if not set</li>
114         * <li>Sets the dictionary entry (if blank) on each of the items to the
115         * collection class</li>
116         * </ul>
117         *
118         * @see org.kuali.rice.krad.uif.component.ComponentBase#performInitialization(org.kuali.rice.krad.uif.view.View,
119         *      java.lang.Object)
120         */
121        @Override
122        public void performInitialization(View view, Object model) {
123            setFieldBindingObjectPath(getBindingInfo().getBindingObjectPath());
124    
125            super.performInitialization(view, model);
126    
127            if (bindingInfo != null) {
128                bindingInfo.setDefaults(view, getPropertyName());
129            }
130    
131            if (addLineBindingInfo != null) {
132                // add line binds to model property
133                if (StringUtils.isNotBlank(addLinePropertyName)) {
134                    addLineBindingInfo.setDefaults(view, getPropertyName());
135                    addLineBindingInfo.setBindingName(addLinePropertyName);
136                    if (StringUtils.isNotBlank(getFieldBindByNamePrefix())) {
137                        addLineBindingInfo.setBindByNamePrefix(getFieldBindByNamePrefix());
138                    }
139                }
140            }
141    
142            for (Component item : getItems()) {
143                if (item instanceof DataField) {
144                    DataField field = (DataField) item;
145    
146                    if (StringUtils.isBlank(field.getDictionaryObjectEntry())) {
147                        field.setDictionaryObjectEntry(collectionObjectClass.getName());
148                    }
149                }
150            }
151    
152            if ((addLineFields == null) || addLineFields.isEmpty()) {
153                addLineFields = getItems();
154            }
155    
156            // if active collection filter not set use default
157            if (this.activeCollectionFilter == null) {
158                activeCollectionFilter = new ActiveCollectionFilter();
159            }
160    
161            // set static collection path on items
162            String collectionPath = "";
163            if (StringUtils.isNotBlank(getBindingInfo().getCollectionPath())) {
164                collectionPath += getBindingInfo().getCollectionPath() + ".";
165            }
166            if (StringUtils.isNotBlank(getBindingInfo().getBindByNamePrefix())) {
167                collectionPath += getBindingInfo().getBindByNamePrefix() + ".";
168            }
169            collectionPath += getBindingInfo().getBindingName();
170    
171            List<DataField> collectionFields = ComponentUtils.getComponentsOfTypeDeep(getItems(), DataField.class);
172            for (DataField collectionField : collectionFields) {
173                collectionField.getBindingInfo().setCollectionPath(collectionPath);
174            }
175    
176            // add collection entry to abstract classes
177            if (!view.getAbstractTypeClasses().containsKey(collectionPath)) {
178                view.getAbstractTypeClasses().put(collectionPath, getCollectionObjectClass());
179            }
180    
181            // initialize container items and sub-collections (since they are not in
182            // child list)
183            for (Component item : getItems()) {
184                view.getViewHelperService().performComponentInitialization(view, model, item);
185            }
186    
187            for (CollectionGroup collectionGroup : getSubCollections()) {
188                collectionGroup.getBindingInfo().setCollectionPath(collectionPath);
189                view.getViewHelperService().performComponentInitialization(view, model, collectionGroup);
190            }
191        }
192    
193        /**
194         * Calls the configured <code>CollectionGroupBuilder</code> to build the
195         * necessary components based on the collection data
196         *
197         * @see org.kuali.rice.krad.uif.container.ContainerBase#performApplyModel(org.kuali.rice.krad.uif.view.View,
198         *      java.lang.Object, org.kuali.rice.krad.uif.component.Component)
199         */
200        @Override
201        public void performApplyModel(View view, Object model, Component parent) {
202            super.performApplyModel(view, model, parent);
203    
204            pushCollectionGroupToReference();
205    
206            // if rendering the collection group, build out the lines
207            if (isRender()) {
208                getCollectionGroupBuilder().build(view, model, this);
209            }
210    
211            // TODO: is this necessary to call again?
212            pushCollectionGroupToReference();
213        }
214    
215        /**
216         * Sets a reference in the context map for all nested components to the collection group
217         * instance, and sets name as parameter for an action fields in the group
218         */
219        protected void pushCollectionGroupToReference() {
220            List<Component> components = this.getComponentsForLifecycle();
221    
222            ComponentUtils.pushObjectToContext(components, UifConstants.ContextVariableNames.COLLECTION_GROUP, this);
223    
224            List<ActionField> actionFields = ComponentUtils.getComponentsOfTypeDeep(components, ActionField.class);
225            for (ActionField actionField : actionFields) {
226                actionField.addActionParameter(UifParameters.SELLECTED_COLLECTION_PATH,
227                        this.getBindingInfo().getBindingPath());
228            }
229        }
230    
231        /**
232         * New collection lines are handled in the framework by maintaining a map on
233         * the form. The map contains as a key the collection name, and as value an
234         * instance of the collection type. An entry is created here for the
235         * collection represented by the <code>CollectionGroup</code> if an instance
236         * is not available (clearExistingLine will force a new instance). The given
237         * model must be a subclass of <code>UifFormBase</code> in order to find the
238         * Map.
239         *
240         * @param model - Model instance that contains the new collection lines Map
241         * @param clearExistingLine - boolean that indicates whether the line should be set to a
242         * new instance if it already exists
243         */
244        public void initializeNewCollectionLine(View view, Object model, CollectionGroup collectionGroup,
245                boolean clearExistingLine) {
246            getCollectionGroupBuilder().initializeNewCollectionLine(view, model, collectionGroup, clearExistingLine);
247        }
248    
249        /**
250         * @see org.kuali.rice.krad.uif.container.ContainerBase#getComponentsForLifecycle()
251         */
252        @Override
253        public List<Component> getComponentsForLifecycle() {
254            List<Component> components = super.getComponentsForLifecycle();
255    
256            components.add(addLineLabelField);
257            components.add(collectionLookup);
258    
259            // remove the containers items because we don't want them as children
260            // (they will become children of the layout manager as the rows are
261            // created)
262            for (Component item : getItems()) {
263                if (components.contains(item)) {
264                    components.remove(item);
265                }
266            }
267    
268            return components;
269        }
270    
271        /**
272         * @see org.kuali.rice.krad.uif.component.Component#getComponentPrototypes()
273         */
274        @Override
275        public List<Component> getComponentPrototypes() {
276            List<Component> components = super.getComponentPrototypes();
277    
278            components.addAll(actionFields);
279            components.addAll(addLineActionFields);
280            components.addAll(getItems());
281            components.addAll(getSubCollections());
282    
283            return components;
284        }
285    
286        /**
287         * Object class the collection maintains. Used to get dictionary information
288         * in addition to creating new instances for the collection when necessary
289         *
290         * @return Class<?> collection object class
291         */
292        public Class<?> getCollectionObjectClass() {
293            return this.collectionObjectClass;
294        }
295    
296        /**
297         * Setter for the collection object class
298         *
299         * @param collectionObjectClass
300         */
301        public void setCollectionObjectClass(Class<?> collectionObjectClass) {
302            this.collectionObjectClass = collectionObjectClass;
303        }
304    
305        /**
306         * @see org.kuali.rice.krad.uif.component.DataBinding#getPropertyName()
307         */
308        public String getPropertyName() {
309            return this.propertyName;
310        }
311    
312        /**
313         * Setter for the collections property name
314         *
315         * @param propertyName
316         */
317        public void setPropertyName(String propertyName) {
318            this.propertyName = propertyName;
319        }
320    
321        /**
322         * Determines the binding path for the collection. Used to get the
323         * collection value from the model in addition to setting the binding path
324         * for the collection attributes
325         *
326         * @see org.kuali.rice.krad.uif.component.DataBinding#getBindingInfo()
327         */
328        public BindingInfo getBindingInfo() {
329            return this.bindingInfo;
330        }
331    
332        /**
333         * Setter for the binding info instance
334         *
335         * @param bindingInfo
336         */
337        public void setBindingInfo(BindingInfo bindingInfo) {
338            this.bindingInfo = bindingInfo;
339        }
340    
341        /**
342         * Action fields that should be rendered for each collection line. Example
343         * line action is the delete action
344         *
345         * @return List<ActionField> line action fields
346         */
347        public List<ActionField> getActionFields() {
348            return this.actionFields;
349        }
350    
351        /**
352         * Setter for the line action fields list
353         *
354         * @param actionFields
355         */
356        public void setActionFields(List<ActionField> actionFields) {
357            this.actionFields = actionFields;
358        }
359    
360        /**
361         * Indicates whether the action column for the collection should be rendered
362         *
363         * @return boolean true if the actions should be rendered, false if not
364         * @see #getActionFields()
365         */
366        public boolean isRenderLineActions() {
367            return this.renderLineActions;
368        }
369    
370        /**
371         * Setter for the render line actions indicator
372         *
373         * @param renderLineActions
374         */
375        public void setRenderLineActions(boolean renderLineActions) {
376            this.renderLineActions = renderLineActions;
377        }
378    
379        /**
380         * Indicates whether an add line should be rendered for the collection
381         *
382         * @return boolean true if add line should be rendered, false if it should
383         *         not be
384         */
385        public boolean isRenderAddLine() {
386            return this.renderAddLine;
387        }
388    
389        /**
390         * Setter for the render add line indicator
391         *
392         * @param renderAddLine
393         */
394        public void setRenderAddLine(boolean renderAddLine) {
395            this.renderAddLine = renderAddLine;
396        }
397    
398        /**
399         * Convenience getter for the add line label field text. The text is used to
400         * label the add line when rendered and its placement depends on the
401         * <code>LayoutManager</code>.
402         * <p>
403         * For the <code>TableLayoutManager</code> the label appears in the sequence
404         * column to the left of the add line fields. For the
405         * <code>StackedLayoutManager</code> the label is placed into the group
406         * header for the line.
407         * </p>
408         *
409         * @return String add line label
410         */
411        public String getAddLineLabel() {
412            if (getAddLineLabelField() != null) {
413                return getAddLineLabelField().getLabelText();
414            }
415    
416            return null;
417        }
418    
419        /**
420         * Setter for the add line label text
421         *
422         * @param addLineLabel
423         */
424        public void setAddLineLabel(String addLineLabel) {
425            if (getAddLineLabelField() != null) {
426                getAddLineLabelField().setLabelText(addLineLabel);
427            }
428        }
429    
430        /**
431         * <code>LabelField</code> instance for the add line label
432         *
433         * @return LabelField add line label field
434         * @see #getAddLineLabel()
435         */
436        public LabelField getAddLineLabelField() {
437            return this.addLineLabelField;
438        }
439    
440        /**
441         * Setter for the <code>LabelField</code> instance for the add line label
442         *
443         * @param addLineLabelField
444         * @see #getAddLineLabel()
445         */
446        public void setAddLineLabelField(LabelField addLineLabelField) {
447            this.addLineLabelField = addLineLabelField;
448        }
449    
450        /**
451         * Name of the property that contains an instance for the add line. If set
452         * this is used with the binding info to create the path to the add line.
453         * Can be left blank in which case the framework will manage the add line
454         * instance in a generic map.
455         *
456         * @return String add line property name
457         */
458        public String getAddLinePropertyName() {
459            return this.addLinePropertyName;
460        }
461    
462        /**
463         * Setter for the add line property name
464         *
465         * @param addLinePropertyName
466         */
467        public void setAddLinePropertyName(String addLinePropertyName) {
468            this.addLinePropertyName = addLinePropertyName;
469        }
470    
471        /**
472         * <code>BindingInfo</code> instance for the add line property used to
473         * determine the full binding path. If add line name given
474         * {@link #getAddLineLabel()} then it is set as the binding name on the
475         * binding info. Add line label and binding info are not required, in which
476         * case the framework will manage the new add line instances through a
477         * generic map (model must extend UifFormBase)
478         *
479         * @return BindingInfo add line binding info
480         */
481        public BindingInfo getAddLineBindingInfo() {
482            return this.addLineBindingInfo;
483        }
484    
485        /**
486         * Setter for the add line binding info
487         *
488         * @param addLineBindingInfo
489         */
490        public void setAddLineBindingInfo(BindingInfo addLineBindingInfo) {
491            this.addLineBindingInfo = addLineBindingInfo;
492        }
493    
494        /**
495         * List of <code>Component</code> instances that should be rendered for the
496         * collection add line (if enabled). If not set, the default group's items
497         * list will be used
498         *
499         * @return List<? extends Component> add line field list
500         */
501        public List<? extends Component> getAddLineFields() {
502            return this.addLineFields;
503        }
504    
505        /**
506         * Setter for the add line field list
507         *
508         * @param addLineFields
509         */
510        public void setAddLineFields(List<? extends Component> addLineFields) {
511            this.addLineFields = addLineFields;
512        }
513    
514        /**
515         * Action fields that should be rendered for the add line. This is generally
516         * the add action (button) but can be configured to contain additional
517         * actions
518         *
519         * @return List<ActionField> add line action fields
520         */
521        public List<ActionField> getAddLineActionFields() {
522            return this.addLineActionFields;
523        }
524    
525        /**
526         * Setter for the add line action fields
527         *
528         * @param addLineActionFields
529         */
530        public void setAddLineActionFields(List<ActionField> addLineActionFields) {
531            this.addLineActionFields = addLineActionFields;
532        }
533    
534        /**
535         * Indicates whether lines of the collection group should be selected by rendering a
536         * field for each line that will allow selection
537         *
538         * <p>
539         * For example, having the select field enabled could allow selecting multiple lines from a search
540         * to return (multi-value lookup)
541         * </p>
542         *
543         * @return boolean true if select field should be rendered, false if not
544         */
545        public boolean isRenderSelectField() {
546            return renderSelectField;
547        }
548    
549        /**
550         * Setter for the render selected field indicator
551         *
552         * @param renderSelectField
553         */
554        public void setRenderSelectField(boolean renderSelectField) {
555            this.renderSelectField = renderSelectField;
556        }
557    
558        /**
559         * When {@link #isRenderSelectField()} is true, gives the name of the property the select field
560         * should bind to
561         *
562         * <p>
563         * Note if no prefix is given in the property name, such as 'form.', it is assumed the property is
564         * contained on the collection line. In this case the binding path to the collection line will be
565         * appended. In other cases, it is assumed the property is a list or set of String that will hold the
566         * selected identifier strings
567         * </p>
568         *
569         * <p>
570         * This property is not required. If not the set the framework will use a property contained on
571         * <code>UifFormBase</code>
572         * </p>
573         *
574         * @return String property name for select field
575         */
576        public String getSelectPropertyName() {
577            return selectPropertyName;
578        }
579    
580        /**
581         * Setter for the property name that will bind to the select field
582         *
583         * @param selectPropertyName
584         */
585        public void setSelectPropertyName(String selectPropertyName) {
586            this.selectPropertyName = selectPropertyName;
587        }
588    
589        /**
590         * Instance of the <code>QuickFinder</code> widget that configures a multi-value lookup for the collection
591         *
592         * <p>
593         * If the collection lookup is enabled (by the render property of the quick finder), {@link
594         * #getCollectionObjectClass()} will be used as the data object class for the lookup (if not set). Field
595         * conversions need to be set as usual and will be applied for each line returned
596         * </p>
597         *
598         * @return QuickFinder instance configured for the collection lookup
599         */
600        public QuickFinder getCollectionLookup() {
601            return collectionLookup;
602        }
603    
604        /**
605         * Setter for the collection lookup quickfinder instance
606         *
607         * @param collectionLookup
608         */
609        public void setCollectionLookup(QuickFinder collectionLookup) {
610            this.collectionLookup = collectionLookup;
611        }
612    
613        /**
614         * Indicates whether inactive collections lines should be displayed
615         *
616         * <p>
617         * Setting only applies when the collection line type implements the
618         * <code>Inactivatable</code> interface. If true and showInactive is
619         * set to false, the collection will be filtered to remove any items
620         * whose active status returns false
621         * </p>
622         *
623         * @return boolean true to show inactive records, false to not render inactive records
624         */
625        public boolean isShowInactive() {
626            return showInactive;
627        }
628    
629        /**
630         * Setter for the show inactive indicator
631         *
632         * @param showInactive boolean show inactive
633         */
634        public void setShowInactive(boolean showInactive) {
635            this.showInactive = showInactive;
636        }
637    
638        /**
639         * Collection filter instance for filtering the collection data when the
640         * showInactive flag is set to false
641         *
642         * @return CollectionFilter
643         */
644        public CollectionFilter getActiveCollectionFilter() {
645            return activeCollectionFilter;
646        }
647    
648        /**
649         * Setter for the collection filter to use for filter inactive records from the
650         * collection
651         *
652         * @param activeCollectionFilter - CollectionFilter instance
653         */
654        public void setActiveCollectionFilter(CollectionFilter activeCollectionFilter) {
655            this.activeCollectionFilter = activeCollectionFilter;
656        }
657    
658        /**
659         * List of {@link CollectionFilter} instances that should be invoked to filter the collection before
660         * displaying
661         *
662         * @return List<CollectionFilter>
663         */
664        public List<CollectionFilter> getFilters() {
665            return filters;
666        }
667    
668        /**
669         * Setter for the List of collection filters for which the collection will be filtered against
670         *
671         * @param filters
672         */
673        public void setFilters(List<CollectionFilter> filters) {
674            this.filters = filters;
675        }
676    
677        /**
678         * List of <code>CollectionGroup</code> instances that are sub-collections
679         * of the collection represented by this collection group
680         *
681         * @return List<CollectionGroup> sub collections
682         */
683        public List<CollectionGroup> getSubCollections() {
684            return this.subCollections;
685        }
686    
687        /**
688         * Setter for the sub collection list
689         *
690         * @param subCollections
691         */
692        public void setSubCollections(List<CollectionGroup> subCollections) {
693            this.subCollections = subCollections;
694        }
695    
696        /**
697         * Suffix for IDs that identifies the collection line the sub-collection belongs to
698         *
699         * <p>
700         * Built by the framework as the collection lines are being generated
701         * </p>
702         *
703         * @return String id suffix for sub-collection
704         */
705        public String getSubCollectionSuffix() {
706            return subCollectionSuffix;
707        }
708    
709        /**
710         * Setter for the sub-collection suffix (used by framework, should not be
711         * set in configuration)
712         *
713         * @param subCollectionSuffix
714         */
715        public void setSubCollectionSuffix(String subCollectionSuffix) {
716            this.subCollectionSuffix = subCollectionSuffix;
717        }
718    
719        /**
720         * Collection Security object that indicates what authorization (permissions) exist for the collection
721         *
722         * @return CollectionGroupSecurity instance
723         */
724        @Override
725        public CollectionGroupSecurity getComponentSecurity() {
726            return collectionGroupSecurity;
727        }
728    
729        /**
730         * Setter for the collection groups security object
731         *
732         * @param collectionGroupSecurity
733         */
734        public void setComponentSecurity(CollectionGroupSecurity collectionGroupSecurity) {
735            this.collectionGroupSecurity = collectionGroupSecurity;
736        }
737    
738        /**
739         * <code>CollectionGroupBuilder</code> instance that will build the
740         * components dynamically for the collection instance
741         *
742         * @return CollectionGroupBuilder instance
743         */
744        public CollectionGroupBuilder getCollectionGroupBuilder() {
745            if (this.collectionGroupBuilder == null) {
746                this.collectionGroupBuilder = new CollectionGroupBuilder();
747            }
748            return this.collectionGroupBuilder;
749        }
750    
751        /**
752         * Setter for the collection group building instance
753         *
754         * @param collectionGroupBuilder
755         */
756        public void setCollectionGroupBuilder(CollectionGroupBuilder collectionGroupBuilder) {
757            this.collectionGroupBuilder = collectionGroupBuilder;
758        }
759    
760        /**
761         * @param showHideInactiveButton the showHideInactiveButton to set
762         */
763        public void setShowHideInactiveButton(boolean showHideInactiveButton) {
764            this.showHideInactiveButton = showHideInactiveButton;
765        }
766    
767        /**
768         * @return the showHideInactiveButton
769         */
770        public boolean isShowHideInactiveButton() {
771            return showHideInactiveButton;
772        }
773    
774    }