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 }