View Javadoc

1   /*
2    * Copyright 2007 The Kuali Foundation Licensed under the Educational Community
3    * License, Version 1.0 (the "License"); you may not use this file except in
4    * compliance with the License. You may obtain a copy of the License at
5    * http://www.opensource.org/licenses/ecl1.php Unless required by applicable law
6    * or agreed to in writing, software distributed under the License is
7    * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
8    * KIND, either express or implied. See the License for the specific language
9    * governing permissions and limitations under the License.
10   */
11  package org.kuali.rice.krad.uif.container;
12  
13  import java.util.ArrayList;
14  import java.util.List;
15  
16  import org.apache.commons.lang.StringUtils;
17  import org.codehaus.jackson.map.ObjectMapper;
18  import org.kuali.rice.core.api.mo.common.active.Inactivatable;
19  import org.kuali.rice.krad.uif.UifConstants;
20  import org.kuali.rice.krad.uif.UifParameters;
21  import org.kuali.rice.krad.uif.component.BindingInfo;
22  import org.kuali.rice.krad.uif.component.Component;
23  import org.kuali.rice.krad.uif.component.DataBinding;
24  import org.kuali.rice.krad.uif.field.ActionField;
25  import org.kuali.rice.krad.uif.field.AttributeField;
26  import org.kuali.rice.krad.uif.field.Field;
27  import org.kuali.rice.krad.uif.field.LabelField;
28  import org.kuali.rice.krad.uif.util.ComponentUtils;
29  import org.kuali.rice.krad.uif.view.View;
30  import org.kuali.rice.krad.uif.widget.QuickFinder;
31  
32  /**
33   * Group that holds a collection of objects and configuration for presenting the
34   * collection in the UI. Supports functionality such as add line, line actions,
35   * and nested collections.
36   * 
37   * <p>
38   * Note the standard header/footer can be used to give a header to the
39   * collection as a whole, or to provide actions that apply to the entire
40   * collection
41   * </p>
42   * 
43   * <p>
44   * For binding purposes the binding path of each row field is indexed. The name
45   * property inherited from <code>ComponentBase</code> is used as the collection
46   * name. The collectionObjectClass property is used to lookup attributes from
47   * the data dictionary.
48   * </p>
49   *
50   * @author Kuali Rice Team (rice.collab@kuali.org)
51   */
52  public class CollectionGroup extends Group implements DataBinding {
53      private static final long serialVersionUID = -6496712566071542452L;
54  
55      private Class<?> collectionObjectClass;
56  
57      private String propertyName;
58      private BindingInfo bindingInfo;
59  
60      private boolean renderAddLine;
61      private String addLinePropertyName;
62      private BindingInfo addLineBindingInfo;
63      private LabelField addLineLabelField;
64      private List<? extends Field> addLineFields;
65      private List<ActionField> addLineActionFields;
66  
67      private boolean renderLineActions;
68      private List<ActionField> actionFields;
69  
70      private boolean renderSelectField;
71      private String selectPropertyName;
72  
73      private QuickFinder collectionLookup;
74  
75      private boolean showHideInactiveButton;
76      private boolean showInactive;
77      private CollectionFilter activeCollectionFilter;
78  
79      private List<CollectionGroup> subCollections;
80      private String subCollectionSuffix;
81  
82      private CollectionGroupBuilder collectionGroupBuilder;
83  
84      public CollectionGroup() {
85          renderAddLine = true;
86          renderLineActions = true;
87          showInactive = false;
88          showHideInactiveButton = true;
89          renderSelectField = false;
90  
91          actionFields = new ArrayList<ActionField>();
92          addLineFields = new ArrayList<Field>();
93          addLineActionFields = new ArrayList<ActionField>();
94          subCollections = new ArrayList<CollectionGroup>();
95      }
96  
97      /**
98       * The following actions are performed:
99       *
100      * <ul>
101      * <li>Set fieldBindModelPath to the collection model path (since the fields
102      * have to belong to the same model as the collection)</li>
103      * <li>Set defaults for binding</li>
104      * <li>Default add line field list to groups items list</li>
105      * <li>Sets default active collection filter if not set</li>
106      * <li>Sets the dictionary entry (if blank) on each of the items to the
107      * collection class</li>
108      * </ul>
109      *
110      * @see org.kuali.rice.krad.uif.component.ComponentBase#performInitialization(org.kuali.rice.krad.uif.view.View, java.lang.Object)
111      */
112     @Override
113     public void performInitialization(View view, Object model) {
114         setFieldBindingObjectPath(getBindingInfo().getBindingObjectPath());
115 
116         super.performInitialization(view, model);
117 
118         if (bindingInfo != null) {
119             bindingInfo.setDefaults(view, getPropertyName());
120         }
121 
122         if (addLineBindingInfo != null) {
123             // add line binds to model property
124             if (StringUtils.isNotBlank(addLinePropertyName)) {
125                 addLineBindingInfo.setDefaults(view, getPropertyName());
126                 addLineBindingInfo.setBindingName(addLinePropertyName);
127                 if (StringUtils.isNotBlank(getFieldBindByNamePrefix())) {
128                     addLineBindingInfo.setBindByNamePrefix(getFieldBindByNamePrefix());
129                 }
130             }
131         }
132 
133         for (Component item : getItems()) {
134             if (item instanceof AttributeField) {
135                 AttributeField field = (AttributeField) item;
136 
137                 if (StringUtils.isBlank(field.getDictionaryObjectEntry())) {
138                     field.setDictionaryObjectEntry(collectionObjectClass.getName());
139                 }
140             }
141         }
142         
143         if ((addLineFields == null) || addLineFields.isEmpty()) {
144             addLineFields = getItems();
145         }
146 
147         // if active collection filter not set use default
148         if (this.activeCollectionFilter == null) {
149             activeCollectionFilter = new ActiveCollectionFilter();
150         }
151         
152         // set static collection path on items
153         String collectionPath = "";
154         if (StringUtils.isNotBlank(getBindingInfo().getCollectionPath())) {
155             collectionPath += getBindingInfo().getCollectionPath() + ".";
156         }
157         if (StringUtils.isNotBlank(getBindingInfo().getBindByNamePrefix())) {
158             collectionPath += getBindingInfo().getBindByNamePrefix() + ".";
159         }
160         collectionPath += getBindingInfo().getBindingName();
161         
162         List<AttributeField> collectionFields = ComponentUtils.getComponentsOfTypeDeep(getItems(), AttributeField.class);
163         for (AttributeField collectionField : collectionFields) {
164             collectionField.getBindingInfo().setCollectionPath(collectionPath);
165         }
166 
167         // add collection entry to abstract classes
168         if (!view.getAbstractTypeClasses().containsKey(collectionPath)) {
169             view.getAbstractTypeClasses().put(collectionPath, getCollectionObjectClass());
170         }
171 
172         // initialize container items and sub-collections (since they are not in
173         // child list)
174         for (Component item : getItems()) {
175             view.getViewHelperService().performComponentInitialization(view, model, item);
176         }
177 
178         for (CollectionGroup collectionGroup : getSubCollections()) {
179             collectionGroup.getBindingInfo().setCollectionPath(collectionPath);
180             view.getViewHelperService().performComponentInitialization(view, model, collectionGroup);
181         }
182     }
183 
184     /**
185      * Calls the configured <code>CollectionGroupBuilder</code> to build the
186      * necessary components based on the collection data
187      * 
188      * @see org.kuali.rice.krad.uif.container.ContainerBase#performApplyModel(org.kuali.rice.krad.uif.view.View,
189      *      java.lang.Object, org.kuali.rice.krad.uif.component.Component)
190      */
191     @Override
192     public void performApplyModel(View view, Object model, Component parent) {
193         super.performApplyModel(view, model, parent);
194 
195         pushCollectionGroupToReference();
196 
197         getCollectionGroupBuilder().build(view, model, this);
198 
199         // TODO: is this necessary to call again?
200         pushCollectionGroupToReference();
201     }
202 
203     /**
204      * Sets a reference in the context map for all nested components to the collection group
205      * instance, and sets name as parameter for an action fields in the group
206      */
207     protected void pushCollectionGroupToReference() {
208         List<Component> components = this.getNestedComponents();
209         
210         ComponentUtils.pushObjectToContext(components, UifConstants.ContextVariableNames.COLLECTION_GROUP, this);
211 
212         List<ActionField> actionFields =
213                 ComponentUtils.getComponentsOfTypeDeep(components, ActionField.class);
214         for (ActionField actionField : actionFields) {
215             actionField.addActionParameter(UifParameters.SELLECTED_COLLECTION_PATH,
216                     this.getBindingInfo().getBindingPath());
217         }
218     }
219 
220     /**
221      * Performs any filtering necessary on the collection before building the collection fields
222      *
223      * <p>
224      * If showInactive is set to false and the collection line type implements <code>Inactivatable</code>,
225      * invokes the active collection filer
226      * </p>
227      *
228      * @param model - object containing the views data, from which the collection will be pulled
229      */
230     protected List<Integer> performCollectionFiltering(View view, Object model) {
231         if (Inactivatable.class.isAssignableFrom(this.collectionObjectClass) && !showInactive) {
232             return this.activeCollectionFilter.filter(view, model, this);
233         }else{
234             return null;
235         }
236     }
237 
238     /**
239      * New collection lines are handled in the framework by maintaining a map on
240      * the form. The map contains as a key the collection name, and as value an
241      * instance of the collection type. An entry is created here for the
242      * collection represented by the <code>CollectionGroup</code> if an instance
243      * is not available (clearExistingLine will force a new instance). The given
244      * model must be a subclass of <code>UifFormBase</code> in order to find the
245      * Map.
246      * 
247      * @param model
248      *            - Model instance that contains the new collection lines Map
249      * @param clearExistingLine
250      *            - boolean that indicates whether the line should be set to a
251      *            new instance if it already exists
252      */
253     public void initializeNewCollectionLine(View view, Object model, CollectionGroup collectionGroup,
254             boolean clearExistingLine) {
255         getCollectionGroupBuilder().initializeNewCollectionLine(view, model, collectionGroup, clearExistingLine);
256     }
257 
258     /**
259      * @see org.kuali.rice.krad.uif.container.ContainerBase#getNestedComponents()
260      */
261     @Override
262     public List<Component> getNestedComponents() {
263         List<Component> components = super.getNestedComponents();
264 
265         components.add(addLineLabelField);
266         components.addAll(actionFields);
267         components.addAll(addLineActionFields);
268         components.add(collectionLookup);
269 
270         // remove the containers items because we don't want them as children
271         // (they will become children of the layout manager as the rows are
272         // created)
273         for (Component item : getItems()) {
274             if (components.contains(item)) {
275                 components.remove(item);
276             }
277         }
278 
279         return components;
280     }
281 
282     /**
283      * Object class the collection maintains. Used to get dictionary information
284      * in addition to creating new instances for the collection when necessary
285      * 
286      * @return Class<?> collection object class
287      */
288     public Class<?> getCollectionObjectClass() {
289         return this.collectionObjectClass;
290     }
291 
292     /**
293      * Setter for the collection object class
294      * 
295      * @param collectionObjectClass
296      */
297     public void setCollectionObjectClass(Class<?> collectionObjectClass) {
298         this.collectionObjectClass = collectionObjectClass;
299     }
300 
301     /**
302      * @see org.kuali.rice.krad.uif.component.DataBinding#getPropertyName()
303      */
304     public String getPropertyName() {
305         return this.propertyName;
306     }
307 
308     /**
309      * Setter for the collections property name
310      * 
311      * @param propertyName
312      */
313     public void setPropertyName(String propertyName) {
314         this.propertyName = propertyName;
315     }
316 
317     /**
318      * Determines the binding path for the collection. Used to get the
319      * collection value from the model in addition to setting the binding path
320      * for the collection attributes
321      * 
322      * @see org.kuali.rice.krad.uif.component.DataBinding#getBindingInfo()
323      */
324     public BindingInfo getBindingInfo() {
325         return this.bindingInfo;
326     }
327 
328     /**
329      * Setter for the binding info instance
330      * 
331      * @param bindingInfo
332      */
333     public void setBindingInfo(BindingInfo bindingInfo) {
334         this.bindingInfo = bindingInfo;
335     }
336 
337     /**
338      * Action fields that should be rendered for each collection line. Example
339      * line action is the delete action
340      * 
341      * @return List<ActionField> line action fields
342      */
343     public List<ActionField> getActionFields() {
344         return this.actionFields;
345     }
346 
347     /**
348      * Setter for the line action fields list
349      * 
350      * @param actionFields
351      */
352     public void setActionFields(List<ActionField> actionFields) {
353         this.actionFields = actionFields;
354     }
355 
356     /**
357      * Indicates whether the action column for the collection should be rendered
358      * 
359      * @return boolean true if the actions should be rendered, false if not
360      * @see #getActionFields()
361      */
362     public boolean isRenderLineActions() {
363         return this.renderLineActions;
364     }
365 
366     /**
367      * Setter for the render line actions indicator
368      * 
369      * @param renderLineActions
370      */
371     public void setRenderLineActions(boolean renderLineActions) {
372         this.renderLineActions = renderLineActions;
373     }
374 
375     /**
376      * Indicates whether an add line should be rendered for the collection
377      * 
378      * @return boolean true if add line should be rendered, false if it should
379      *         not be
380      */
381     public boolean isRenderAddLine() {
382         return this.renderAddLine;
383     }
384 
385     /**
386      * Setter for the render add line indicator
387      * 
388      * @param renderAddLine
389      */
390     public void setRenderAddLine(boolean renderAddLine) {
391         this.renderAddLine = renderAddLine;
392     }
393 
394     /**
395      * Convenience getter for the add line label field text. The text is used to
396      * label the add line when rendered and its placement depends on the
397      * <code>LayoutManager</code>.
398      * <p>
399      * For the <code>TableLayoutManager</code> the label appears in the sequence
400      * column to the left of the add line fields. For the
401      * <code>StackedLayoutManager</code> the label is placed into the group
402      * header for the line.
403      * </p>
404      * 
405      * @return String add line label
406      */
407     public String getAddLineLabel() {
408         if (getAddLineLabelField() != null) {
409             return getAddLineLabelField().getLabelText();
410         }
411 
412         return null;
413     }
414 
415     /**
416      * Setter for the add line label text
417      * 
418      * @param addLineLabel
419      */
420     public void setAddLineLabel(String addLineLabel) {
421         if (getAddLineLabelField() != null) {
422             getAddLineLabelField().setLabelText(addLineLabel);
423         }
424     }
425 
426     /**
427      * <code>LabelField</code> instance for the add line label
428      * 
429      * @return LabelField add line label field
430      * @see #getAddLineLabel()
431      */
432     public LabelField getAddLineLabelField() {
433         return this.addLineLabelField;
434     }
435 
436     /**
437      * Setter for the <code>LabelField</code> instance for the add line label
438      * 
439      * @param addLineLabelField
440      * @see #getAddLineLabel()
441      */
442     public void setAddLineLabelField(LabelField addLineLabelField) {
443         this.addLineLabelField = addLineLabelField;
444     }
445 
446     /**
447      * Name of the property that contains an instance for the add line. If set
448      * this is used with the binding info to create the path to the add line.
449      * Can be left blank in which case the framework will manage the add line
450      * instance in a generic map.
451      * 
452      * @return String add line property name
453      */
454     public String getAddLinePropertyName() {
455         return this.addLinePropertyName;
456     }
457 
458     /**
459      * Setter for the add line property name
460      * 
461      * @param addLinePropertyName
462      */
463     public void setAddLinePropertyName(String addLinePropertyName) {
464         this.addLinePropertyName = addLinePropertyName;
465     }
466 
467     /**
468      * <code>BindingInfo</code> instance for the add line property used to
469      * determine the full binding path. If add line name given
470      * {@link #getAddLineLabel()} then it is set as the binding name on the
471      * binding info. Add line label and binding info are not required, in which
472      * case the framework will manage the new add line instances through a
473      * generic map (model must extend UifFormBase)
474      * 
475      * @return BindingInfo add line binding info
476      */
477     public BindingInfo getAddLineBindingInfo() {
478         return this.addLineBindingInfo;
479     }
480 
481     /**
482      * Setter for the add line binding info
483      * 
484      * @param addLineBindingInfo
485      */
486     public void setAddLineBindingInfo(BindingInfo addLineBindingInfo) {
487         this.addLineBindingInfo = addLineBindingInfo;
488     }
489 
490     /**
491      * List of <code>Field</code> instances that should be rendered for the
492      * collection add line (if enabled). If not set, the default group's items
493      * list will be used
494      * 
495      * @return List<? extends Field> add line field list
496      */
497     public List<? extends Field> getAddLineFields() {
498         return this.addLineFields;
499     }
500 
501     /**
502      * Setter for the add line field list
503      * 
504      * @param addLineFields
505      */
506     public void setAddLineFields(List<? extends Field> addLineFields) {
507         this.addLineFields = addLineFields;
508     }
509 
510     /**
511      * Action fields that should be rendered for the add line. This is generally
512      * the add action (button) but can be configured to contain additional
513      * actions
514      * 
515      * @return List<ActionField> add line action fields
516      */
517     public List<ActionField> getAddLineActionFields() {
518         return this.addLineActionFields;
519     }
520 
521     /**
522      * Setter for the add line action fields
523      * 
524      * @param addLineActionFields
525      */
526     public void setAddLineActionFields(List<ActionField> addLineActionFields) {
527         this.addLineActionFields = addLineActionFields;
528     }
529 
530     /**
531      * Indicates whether lines of the collection group should be selected by rendering a
532      * field for each line that will allow selection
533      *
534      * <p>
535      * For example, having the select field enabled could allow selecting multiple lines from a search
536      * to return (multi-value lookup)
537      * </p>
538      *
539      * @return boolean true if select field should be rendered, false if not
540      */
541     public boolean isRenderSelectField() {
542         return renderSelectField;
543     }
544 
545     /**
546      * Setter for the render selected field indicator
547      *
548      * @param renderSelectField
549      */
550     public void setRenderSelectField(boolean renderSelectField) {
551         this.renderSelectField = renderSelectField;
552     }
553 
554     /**
555      * When {@link #isRenderSelectField()} is true, gives the name of the property the select field
556      * should bind to
557      *
558      * <p>
559      * Note if no prefix is given in the property name, such as 'form.', it is assumed the property is
560      * contained on the collection line. In this case the binding path to the collection line will be
561      * appended. In other cases, it is assumed the property is a list or set of String that will hold the
562      * selected identifier strings
563      * </p>
564      *
565      * <p>
566      * This property is not required. If not the set the framework will use a property contained on
567      * <code>UifFormBase</code>
568      * </p>
569      *
570      * @return String property name for select field
571      */
572     public String getSelectPropertyName() {
573         return selectPropertyName;
574     }
575 
576     /**
577      * Setter for the property name that will bind to the select field
578      *
579      * @param selectPropertyName
580      */
581     public void setSelectPropertyName(String selectPropertyName) {
582         this.selectPropertyName = selectPropertyName;
583     }
584 
585     /**
586      * Instance of the <code>QuickFinder</code> widget that configures a multi-value lookup for the collection
587      *
588      * <p>
589      * If the collection lookup is enabled (by the render property of the quick finder), {@link
590      * #getCollectionObjectClass()} will be used as the data object class for the lookup (if not set). Field
591      * conversions need to be set as usual and will be applied for each line returned
592      * </p>
593      *
594      * @return QuickFinder instance configured for the collection lookup
595      */
596     public QuickFinder getCollectionLookup() {
597         return collectionLookup;
598     }
599 
600     /**
601      * Setter for the collection lookup quickfinder instance
602      *
603      * @param collectionLookup
604      */
605     public void setCollectionLookup(QuickFinder collectionLookup) {
606         this.collectionLookup = collectionLookup;
607     }
608 
609     /**
610      * Indicates whether inactive collections lines should be displayed
611      *
612      * <p>
613      * Setting only applies when the collection line type implements the
614      * <code>Inactivatable</code> interface. If true and showInactive is
615      * set to false, the collection will be filtered to remove any items
616      * whose active status returns false
617      * </p>
618      *
619      * @return boolean true to show inactive records, false to not render inactive records
620      */
621     public boolean isShowInactive() {
622         return showInactive;
623     }
624 
625     /**
626      * Setter for the show inactive indicator
627      *
628      * @param showInactive boolean show inactive
629      */
630     public void setShowInactive(boolean showInactive) {
631         this.showInactive = showInactive;
632     }
633 
634     /**
635      * Collection filter instance for filtering the collection data when the
636      * showInactive flag is set to false
637      *
638      * @return CollectionFilter
639      */
640     public CollectionFilter getActiveCollectionFilter() {
641         return activeCollectionFilter;
642     }
643 
644     /**
645      * Setter for the collection filter to use for filter inactive records from the
646      * collection
647      *
648      * @param activeCollectionFilter - CollectionFilter instance
649      */
650     public void setActiveCollectionFilter(CollectionFilter activeCollectionFilter) {
651         this.activeCollectionFilter = activeCollectionFilter;
652     }
653 
654     /**
655      * List of <code>CollectionGroup</code> instances that are sub-collections
656      * of the collection represented by this collection group
657      * 
658      * @return List<CollectionGroup> sub collections
659      */
660     public List<CollectionGroup> getSubCollections() {
661         return this.subCollections;
662     }
663 
664     /**
665      * Setter for the sub collection list
666      * 
667      * @param subCollections
668      */
669     public void setSubCollections(List<CollectionGroup> subCollections) {
670         this.subCollections = subCollections;
671     }
672 
673     /**
674      * Suffix for IDs that identifies the collection line the sub-collection belongs to
675      *
676      * <p>
677      * Built by the framework as the collection lines are being generated
678      * </p>
679      *
680      * @return String id suffix for sub-collection
681      */
682     public String getSubCollectionSuffix() {
683         return subCollectionSuffix;
684     }
685 
686     /**
687      * Setter for the sub-collection suffix (used by framework, should not be
688      * set in configuration)
689      *
690      * @param subCollectionSuffix
691      */
692     public void setSubCollectionSuffix(String subCollectionSuffix) {
693         this.subCollectionSuffix = subCollectionSuffix;
694     }
695 
696     /**
697      * <code>CollectionGroupBuilder</code> instance that will build the
698      * components dynamically for the collection instance
699      * 
700      * @return CollectionGroupBuilder instance
701      */
702     public CollectionGroupBuilder getCollectionGroupBuilder() {
703         if (this.collectionGroupBuilder == null) {
704             this.collectionGroupBuilder = new CollectionGroupBuilder();
705         }
706         return this.collectionGroupBuilder;
707     }
708 
709     /**
710      * Setter for the collection group building instance
711      * 
712      * @param collectionGroupBuilder
713      */
714     public void setCollectionGroupBuilder(CollectionGroupBuilder collectionGroupBuilder) {
715         this.collectionGroupBuilder = collectionGroupBuilder;
716     }
717 
718     /**
719      * @see org.kuali.rice.krad.uif.container.ContainerBase#getItems()
720      */
721     @SuppressWarnings("unchecked")
722     @Override
723     public List<? extends Field> getItems() {
724         return (List<? extends Field>) super.getItems();
725     }
726 
727     /**
728      * @param showHideInactiveButton the showHideInactiveButton to set
729      */
730     public void setShowHideInactiveButton(boolean showHideInactiveButton) {
731         this.showHideInactiveButton = showHideInactiveButton;
732     }
733 
734     /**
735      * @return the showHideInactiveButton
736      */
737     public boolean isShowHideInactiveButton() {
738         return showHideInactiveButton;
739     }
740 
741 }