View Javadoc
1   /**
2    * Copyright 2005-2014 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.uif.container;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.kuali.rice.krad.datadictionary.parse.BeanTag;
20  import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute;
21  import org.kuali.rice.krad.datadictionary.parse.BeanTags;
22  import org.kuali.rice.krad.datadictionary.validator.ValidationTrace;
23  import org.kuali.rice.krad.datadictionary.validator.Validator;
24  import org.kuali.rice.krad.uif.UifConstants;
25  import org.kuali.rice.krad.uif.UifPropertyPaths;
26  import org.kuali.rice.krad.uif.component.Component;
27  import org.kuali.rice.krad.uif.component.DataBinding;
28  import org.kuali.rice.krad.uif.component.DelayedCopy;
29  import org.kuali.rice.krad.uif.field.Field;
30  import org.kuali.rice.krad.uif.field.FieldGroup;
31  import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle;
32  import org.kuali.rice.krad.uif.lifecycle.initialize.AssignIdsTask;
33  import org.kuali.rice.krad.uif.util.LifecycleAwareList;
34  import org.kuali.rice.krad.uif.util.LifecycleElement;
35  import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
36  import org.kuali.rice.krad.uif.view.ExpressionEvaluator;
37  import org.kuali.rice.krad.uif.view.View;
38  import org.kuali.rice.krad.uif.view.ViewModel;
39  import org.kuali.rice.krad.uif.widget.Disclosure;
40  import org.kuali.rice.krad.uif.widget.Scrollpane;
41  import org.kuali.rice.krad.util.KRADUtils;
42  
43  import java.util.ArrayList;
44  import java.util.Collections;
45  import java.util.HashSet;
46  import java.util.List;
47  import java.util.Set;
48  
49  /**
50   * Container that holds a list of <code>Field</code> or other <code>Group</code>
51   * instances
52   *
53   * <p>
54   * Groups can exist at different levels of the <code>View</code>, providing
55   * conceptual groupings such as the page, section, and group. In addition, other
56   * group types can be created to add behavior like collection support
57   * </p>
58   *
59   * <p>
60   * <code>Group</code> implementation has properties for defaulting the binding
61   * information (such as the parent object path and a binding prefix) for the
62   * fields it contains. During the phase these properties (if given) are set on
63   * the fields contained in the <code>Group</code> that implement
64   * <code>DataBinding</code>, unless they have already been set on the field.
65   * </p>
66   *
67   * @author Kuali Rice Team (rice.collab@kuali.org)
68   */
69  @BeanTags({@BeanTag(name = "group-bean", parent = "Uif-GroupBase"),
70          @BeanTag(name = "boxGroupBase-bean", parent = "Uif-BoxGroupBase"),
71          @BeanTag(name = "verticalBoxGroup-bean", parent = "Uif-VerticalBoxGroup"),
72          @BeanTag(name = "verticalBoxSection-bean", parent = "Uif-VerticalBoxSection"),
73          @BeanTag(name = "verticalBoxSubSection-bean", parent = "Uif-VerticalBoxSubSection"),
74          @BeanTag(name = "disclosure-verticalBoxSection-bean", parent = "Uif-Disclosure-VerticalBoxSection"),
75          @BeanTag(name = "disclosure-verticalBoxSubSection-bean", parent = "Uif-Disclosure-VerticalBoxSubSection"),
76          @BeanTag(name = "horizontalBoxGroup-bean", parent = "Uif-HorizontalBoxGroup"),
77          @BeanTag(name = "horizontalBoxSection-bean", parent = "Uif-HorizontalBoxSection"),
78          @BeanTag(name = "horizontalBoxSubSection-bean", parent = "Uif-HorizontalBoxSubSection"),
79          @BeanTag(name = "disclosure-horizontalBoxSection-bean", parent = "Uif-Disclosure-HorizontalBoxSection"),
80          @BeanTag(name = "disclosure-horizontalBoxSubSection-bean", parent = "Uif-Disclosure-HorizontalBoxSubSection"),
81          @BeanTag(name = "gridGroup-bean", parent = "Uif-GridGroup"),
82          @BeanTag(name = "gridSection-bean", parent = "Uif-GridSection"),
83          @BeanTag(name = "gridSubSection-bean", parent = "Uif-GridSubSection"),
84          @BeanTag(name = "disclosure-gridSection-bean", parent = "Uif-Disclosure-GridSection"),
85          @BeanTag(name = "cssGridGroup-bean", parent = "Uif-CssGridGroup"),
86          @BeanTag(name = "cssGridSection-bean", parent = "Uif-CssGridSection"),
87          @BeanTag(name = "cssGridSubSection-bean", parent = "Uif-CssGridSubSection"),
88          @BeanTag(name = "cssGridSection-1FieldLabelColumn-bean", parent = "Uif-CssGridSection-1FieldLabelColumn"),
89          @BeanTag(name = "cssGridSection-2FieldLabelColumn-bean", parent = "Uif-CssGridSection-2FieldLabelColumn"),
90          @BeanTag(name = "cssGridSection-3FieldLabelColumn-bean", parent = "Uif-CssGridSection-3FieldLabelColumn"),
91          @BeanTag(name = "cssGridSubSection-1FieldLabelColumn-bean", parent = "Uif-CssGridSubSection-1FieldLabelColumn"),
92          @BeanTag(name = "cssGridSubSection-2FieldLabelColumn-bean", parent = "Uif-CssGridSubSection-2FieldLabelColumn"),
93          @BeanTag(name = "cssGridSubSection-3FieldLabelColumn-bean", parent = "Uif-CssGridSubSection-3FieldLabelColumn"),
94          @BeanTag(name = "listGroup-bean", parent = "Uif-ListGroup"),
95          @BeanTag(name = "listSection-bean", parent = "Uif-ListSection"),
96          @BeanTag(name = "listSubSection-bean", parent = "Uif-ListSubSection"),
97          @BeanTag(name = "disclosure-listSection-bean", parent = "Uif-Disclosure-ListSection"),
98          @BeanTag(name = "disclosure-listSubSection-bean", parent = "Uif-Disclosure-ListSubSection"),
99          @BeanTag(name = "collectionGridItem-bean", parent = "Uif-CollectionGridItem"),
100         @BeanTag(name = "collectionVerticalBoxItem-bean", parent = "Uif-CollectionVerticalBoxItem"),
101         @BeanTag(name = "collectionHorizontalBoxItem-bean", parent = "Uif-CollectionHorizontalBoxItem"),
102         @BeanTag(name = "headerUpperGroup-bean", parent = "Uif-HeaderUpperGroup"),
103         @BeanTag(name = "headerRightGroup-bean", parent = "Uif-HeaderRightGroup"),
104         @BeanTag(name = "headerLowerGroup-bean", parent = "Uif-HeaderLowerGroup"),
105         @BeanTag(name = "footer-bean", parent = "Uif-FooterBase"),
106         @BeanTag(name = "formFooter-bean", parent = "Uif-FormFooter"),
107         @BeanTag(name = "actionsGroup-bean", parent = "Uif-ActionsGroup"),
108         @BeanTag(name = "disclosureActionsGroup-bean", parent = "Uif-DisclosureActionsGroup"),
109         @BeanTag(name = "disclosureActions-reqMessageGroup-bean", parent = "Uif-DisclosureActions-ReqMessageGroup"),
110         @BeanTag(name = "inactiveItemsActionsGroup-bean", parent = "Uif-InactiveItemsActionsGroup"),
111         @BeanTag(name = "documentInfoGroup-bean", parent = "Uif-DocumentInfoGroup"),
112         @BeanTag(name = "documentOverviewSection-bean", parent = "Uif-DocumentOverviewSection"),
113         @BeanTag(name = "documentAdHocRecipientsSection-bean", parent = "Uif-DocumentAdHocRecipientsSection"),
114         @BeanTag(name = "documentRouteLogSection-bean", parent = "Uif-DocumentRouteLogSection"),
115         @BeanTag(name = "documentPageFooter-bean", parent = "Uif-DocumentPageFooter"),
116         @BeanTag(name = "incidentDetailGroup-bean", parent = "Uif-IncidentDetailGroup"),
117         @BeanTag(name = "incidentStackTraceGroup-bean", parent = "Uif-IncidentStackTraceGroup"),
118         @BeanTag(name = "incidentReportFooter-bean", parent = "Uif-IncidentReportFooter"),
119         @BeanTag(name = "initiatedDocumentFooter-bean", parent = "InitiatedDocumentFooter"),
120         @BeanTag(name = "inquiryFooter-bean", parent = "Uif-InquiryFooter"),
121         @BeanTag(name = "lookupCriteriaGroup-bean", parent = "Uif-LookupCriteriaGroup"),
122         @BeanTag(name = "lookupPageHeaderGroup-bean", parent = "Uif-LookupPageHeaderGroup"),
123         @BeanTag(name = "lookupCriteriaFooter-bean", parent = "Uif-LookupCriteriaFooter"),
124         @BeanTag(name = "lookupResultsFooter-bean", parent = "Uif-LookupResultsFooter"),
125         @BeanTag(name = "maintenanceGridGroup-bean", parent = "Uif-MaintenanceGridGroup"),
126         @BeanTag(name = "maintenanceHorizontalBoxGroup-bean", parent = "Uif-MaintenanceHorizontalBoxGroup"),
127         @BeanTag(name = "maintenanceVerticalBoxGroup-bean", parent = "Uif-MaintenanceVerticalBoxGroup"),
128         @BeanTag(name = "maintenanceGridSection-bean", parent = "Uif-MaintenanceGridSection"),
129         @BeanTag(name = "maintenanceGridSubSection-bean", parent = "Uif-MaintenanceGridSubSection"),
130         @BeanTag(name = "maintenanceHorizontalBoxSection-bean", parent = "Uif-MaintenanceHorizontalBoxSection"),
131         @BeanTag(name = "maintenanceVerticalBoxSection-bean", parent = "Uif-MaintenanceVerticalBoxSection"),
132         @BeanTag(name = "maintenanceHorizontalBoxSubSection-bean", parent = "Uif-MaintenanceHorizontalBoxSubSection"),
133         @BeanTag(name = "maintenanceVerticalBoxSubSection-bean", parent = "Uif-MaintenanceVerticalBoxSubSection")})
134 public class GroupBase extends ContainerBase implements Group {
135     private static final long serialVersionUID = 7953641325356535509L;
136 
137     private String fieldBindByNamePrefix;
138     private String fieldBindingObjectPath;
139 
140     @DelayedCopy
141     private Disclosure disclosure;
142     private Scrollpane scrollpane;
143 
144     private List<? extends Component> items;
145 
146     private String wrapperTag;
147 
148     /**
149      * Default Constructor
150      */
151     public GroupBase() {
152         items = Collections.emptyList();
153     }
154 
155     /**
156      * The following actions are performed:
157      *
158      * <ul>
159      * <li>Sets the bindByNamePrefix if blank on any InputField and
160      * FieldGroup instances within the items List</li>
161      * </ul>
162      *
163      * {@inheritDoc}
164      */
165     @Override
166     public void performInitialization(Object model) {
167         if (isClosedAjaxDisclosure()) {
168             this.setItems(new ArrayList<Component>());
169             this.setFooter(null);
170         }
171 
172         super.performInitialization(model);
173 
174         for (Component component : getItems()) {
175             // append group's field bind by name prefix (if set) to each
176             // attribute field's binding prefix
177             if (component instanceof DataBinding) {
178                 DataBinding dataBinding = (DataBinding) component;
179 
180                 if (StringUtils.isNotBlank(getFieldBindByNamePrefix())) {
181                     String bindByNamePrefixToSet = getFieldBindByNamePrefix();
182 
183                     if (StringUtils.isNotBlank(dataBinding.getBindingInfo().getBindByNamePrefix())) {
184                         bindByNamePrefixToSet += "." + dataBinding.getBindingInfo().getBindByNamePrefix();
185                     }
186                     dataBinding.getBindingInfo().setBindByNamePrefix(bindByNamePrefixToSet);
187                 }
188 
189                 if (StringUtils.isNotBlank(fieldBindingObjectPath) && StringUtils.isBlank(
190                         dataBinding.getBindingInfo().getBindingObjectPath())) {
191                     dataBinding.getBindingInfo().setBindingObjectPath(fieldBindingObjectPath);
192                 }
193             }
194             // set on FieldGroup's group to recursively set AttributeFields
195             else if (component instanceof FieldGroup) {
196                 FieldGroup fieldGroup = (FieldGroup) component;
197 
198                 if (fieldGroup.getGroup() != null) {
199                     if (StringUtils.isBlank(fieldGroup.getGroup().getFieldBindByNamePrefix())) {
200                         fieldGroup.getGroup().setFieldBindByNamePrefix(fieldBindByNamePrefix);
201                     }
202                     if (StringUtils.isBlank(fieldGroup.getGroup().getFieldBindingObjectPath())) {
203                         fieldGroup.getGroup().setFieldBindingObjectPath(fieldBindingObjectPath);
204                     }
205                 }
206             } else if (component instanceof Group) {
207                 Group subGroup = (Group) component;
208                 if (StringUtils.isNotBlank(getFieldBindByNamePrefix())) {
209                     if (StringUtils.isNotBlank(subGroup.getFieldBindByNamePrefix())) {
210                         subGroup.setFieldBindByNamePrefix(
211                                 getFieldBindByNamePrefix() + "." + subGroup.getFieldBindByNamePrefix());
212                     } else {
213                         subGroup.setFieldBindByNamePrefix(getFieldBindByNamePrefix());
214                     }
215                 }
216                 if (StringUtils.isNotBlank(getFieldBindingObjectPath())) {
217                     if (StringUtils.isNotBlank(subGroup.getFieldBindingObjectPath())) {
218                         subGroup.setFieldBindingObjectPath(
219                                 getFieldBindingObjectPath() + "." + subGroup.getFieldBindingObjectPath());
220                     } else {
221                         subGroup.setFieldBindingObjectPath(getFieldBindingObjectPath());
222                     }
223                 }
224             }
225         }
226     }
227 
228     /**
229      * Sets the section boolean to true if this group has a rendering header with text
230      *
231      * {@inheritDoc}
232      */
233     @Override
234     public void performFinalize(Object model, LifecycleElement parent) {
235         super.performFinalize(model, parent);
236 
237         if (StringUtils.isBlank(wrapperTag) && StringUtils.isNotBlank(this.getHeaderText()) && this.getHeader()
238                 .isRender()) {
239             wrapperTag = UifConstants.WrapperTags.SECTION;
240         } else if (StringUtils.isBlank(wrapperTag)) {
241             wrapperTag = UifConstants.WrapperTags.DIV;
242         }
243     }
244 
245     /**
246      * {@inheritDoc}
247      */
248     @Override
249     public Set<Class<? extends Component>> getSupportedComponents() {
250         Set<Class<? extends Component>> supportedComponents = new HashSet<Class<? extends Component>>();
251         supportedComponents.add(Field.class);
252         supportedComponents.add(Group.class);
253 
254         return supportedComponents;
255     }
256 
257     /**
258      * {@inheritDoc}
259      */
260     @Override
261     public String getComponentTypeName() {
262         return "group";
263     }
264 
265     /**
266      * Binding prefix string to set on each of the groups <code>DataField</code> instances
267      *
268      * <p>
269      * As opposed to setting the bindingPrefix on each attribute field instance,
270      * it can be set here for the group. During initialize the string will then
271      * be set on each attribute field instance if the bindingPrefix is blank and
272      * not a form field
273      * </p>
274      *
275      * @return String binding prefix to set
276      */
277     @Override
278     @BeanTagAttribute(name = "fieldBindByNamePrefix")
279     public String getFieldBindByNamePrefix() {
280         return this.fieldBindByNamePrefix;
281     }
282 
283     /**
284      * Setter for the field binding prefix
285      *
286      * @param fieldBindByNamePrefix
287      */
288     public void setFieldBindByNamePrefix(String fieldBindByNamePrefix) {
289         this.fieldBindByNamePrefix = fieldBindByNamePrefix;
290     }
291 
292     /**
293      * Object binding path to set on each of the group's
294      * <code>InputField</code> instances
295      *
296      * <p>
297      * When the attributes of the group belong to a object whose path is
298      * different from the default then this property can be given to set each of
299      * the attributes instead of setting the model path on each one. The object
300      * path can be overridden at the attribute level. The object path is set to
301      * the fieldBindingObjectPath during the initialize phase.
302      * </p>
303      *
304      * @return String model path to set
305      * @see org.kuali.rice.krad.uif.component.BindingInfo#getBindingObjectPath()
306      */
307     @BeanTagAttribute(name = "fieldBindingObjectPath")
308     public String getFieldBindingObjectPath() {
309         return this.fieldBindingObjectPath;
310     }
311 
312     /**
313      * Setter for the field object binding path
314      *
315      * @param fieldBindingObjectPath
316      */
317     public void setFieldBindingObjectPath(String fieldBindingObjectPath) {
318         this.fieldBindingObjectPath = fieldBindingObjectPath;
319     }
320 
321     /**
322      * Disclosure widget that provides collapse/expand functionality for the
323      * group
324      *
325      * @return Disclosure instance
326      */
327     @BeanTagAttribute(name = "Disclosure", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
328     public Disclosure getDisclosure() {
329         return this.disclosure;
330     }
331 
332     /**
333      * Setter for the group's disclosure instance
334      *
335      * @param disclosure
336      */
337     public void setDisclosure(Disclosure disclosure) {
338         this.disclosure = disclosure;
339     }
340 
341     /**
342      * Scrollpane widget that provides scrolling functionality for the
343      * group
344      *
345      * @return Scrollpane instance
346      */
347     @BeanTagAttribute(name = "scrollpane", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
348     public Scrollpane getScrollpane() {
349         return this.scrollpane;
350     }
351 
352     /**
353      * Setter for the group's scrollpane instance
354      *
355      * @param scrollpane
356      */
357     public void setScrollpane(Scrollpane scrollpane) {
358         this.scrollpane = scrollpane;
359     }
360 
361     /**
362      * {@inheritDoc}
363      */
364     @Override
365     @BeanTagAttribute(name = "items", type = BeanTagAttribute.AttributeType.LISTBEAN)
366     public List<? extends Component> getItems() {
367         if (items == Collections.EMPTY_LIST && isMutable(true)) {
368             items = new LifecycleAwareList<Component>(this);
369         }
370 
371         return this.items;
372     }
373 
374     /**
375      * Setter for the Group's list of components
376      *
377      * @param items
378      */
379     @SuppressWarnings("unchecked")
380     @Override
381     public void setItems(List<? extends Component> items) {
382         if (items == null) {
383             this.items = Collections.emptyList();
384         } else if (items.contains(this)) {
385             throw new IllegalArgumentException("Attempted to add group to itself");
386         } else {
387             this.items = new LifecycleAwareList<Component>(this, (List<Component>) items);
388         }
389     }
390 
391     /**
392      * Defines the html tag that will wrap this group, if left blank, this will automatically be set
393      * by the framework to the appropriate tag (in most cases section or div)
394      *
395      * @return the html tag used to wrap this group
396      */
397     @BeanTagAttribute(name = "wrapperTag")
398     public String getWrapperTag() {
399         return wrapperTag;
400     }
401 
402     /**
403      * @see org.kuali.rice.krad.uif.container.GroupBase#getWrapperTag()
404      */
405     public void setWrapperTag(String wrapperTag) {
406         this.wrapperTag = wrapperTag;
407     }
408 
409     /**
410      * Returns true if this group has a Disclosure widget that is currently closed and using ajax disclosure
411      *
412      * @return true if this group has a Disclosure widget that is currently closed and using ajax disclosure
413      */
414     protected boolean isClosedAjaxDisclosure() {
415         ViewModel model = (ViewModel) ViewLifecycle.getModel();
416         View view = ViewLifecycle.getView();
417 
418         if (this.getDisclosure() == null) {
419             return false;
420         }
421 
422         ViewLifecycle.getExpressionEvaluator().populatePropertyExpressionsFromGraph(this, false);
423         // Evaluate the disclosure.defaultOpen expression early so that ajax disclosure mechanisms
424         // can take its state into account
425         if (this.getDisclosure() != null && StringUtils.isNotBlank(this.getDisclosure().getPropertyExpression(
426                 UifPropertyPaths.DEFAULT_OPEN))) {
427             ExpressionEvaluator expressionEvaluator = ViewLifecycle.getExpressionEvaluator();
428 
429             String expression = this.getDisclosure().getPropertyExpression(UifPropertyPaths.DEFAULT_OPEN);
430             expression = expressionEvaluator.replaceBindingPrefixes(view, this, expression);
431 
432             expression = expressionEvaluator.evaluateExpressionTemplate(this.getDisclosure().getContext(), expression);
433             ObjectPropertyUtils.setPropertyValue(this.getDisclosure(), UifPropertyPaths.DEFAULT_OPEN, expression);
434         }
435 
436         if (disclosure.getId() == null) {
437             disclosure.setId(AssignIdsTask.generateId(disclosure, view));
438         }
439 
440         // Ensure that the disclosure has the correct state before evaluate ajax-based placeholder replacement
441         if (this.getDisclosure() != null) {
442             KRADUtils.syncClientSideStateForComponent(this.getDisclosure(), model.getClientStateForSyncing());
443         }
444 
445         boolean closed = !this.getDisclosure().isDefaultOpen();
446 
447         boolean open = (closed && ViewLifecycle.isRefreshComponent(ViewLifecycle.getPhase().getViewPhase(),
448                 this.getViewPath())) || !closed;
449 
450         if (open) {
451             this.getDisclosure().setDefaultOpen(true);
452         }
453 
454         // Considered closed when not a retrieveViaAjax group and not set to open in this phase
455         return !this.isRetrieveViaAjax() && this.getDisclosure().isAjaxRetrievalWhenOpened() && !open;
456     }
457 
458     /**
459      * {@inheritDoc}
460      */
461     @Override
462     public void completeValidation(ValidationTrace tracer) {
463         tracer.addBean(this);
464 
465         // Checks that no invalid items are present
466         for (int i = 0; i < getItems().size(); i++) {
467             if (getItems().get(i).getClass() == PageGroup.class || getItems().get(i).getClass()
468                     == NavigationGroup.class) {
469                 String currentValues[] = {"item(" + i + ").class =" + getItems().get(i).getClass()};
470                 tracer.createError("Items in Group cannot be PageGroup or NaviagtionGroup", currentValues);
471             }
472         }
473 
474         // Checks that the layout manager is set
475         if (getLayoutManager() == null) {
476             if (Validator.checkExpressions(this, "layoutManager")) {
477                 String currentValues[] = {"layoutManager = " + getLayoutManager()};
478                 tracer.createError("LayoutManager must be set", currentValues);
479             }
480         }
481 
482         super.completeValidation(tracer.getCopy());
483     }
484 
485     /**
486      * Determine the group should be rendered on initial load, or if a loading message should be rendered instead.
487      *
488      * @return True if a loading message should be rendered, false if the group should be rendered now.
489      */
490     public boolean isRenderLoading() {
491         return disclosure != null && disclosure.isAjaxRetrievalWhenOpened() && (!disclosure.isRender() || !disclosure
492                 .isDefaultOpen());
493     }
494 
495 }