001/**
002 * Copyright 2005-2018 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 */
016package org.kuali.rice.krad.uif.layout;
017
018import org.apache.commons.lang.StringUtils;
019import org.kuali.rice.krad.datadictionary.parse.BeanTag;
020import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute;
021import org.kuali.rice.krad.datadictionary.parse.BeanTags;
022import org.kuali.rice.krad.uif.UifConstants;
023import org.kuali.rice.krad.uif.UifPropertyPaths;
024import org.kuali.rice.krad.uif.component.Component;
025import org.kuali.rice.krad.uif.component.DataBinding;
026import org.kuali.rice.krad.uif.component.KeepExpression;
027import org.kuali.rice.krad.uif.container.CollectionGroup;
028import org.kuali.rice.krad.uif.container.Container;
029import org.kuali.rice.krad.uif.container.DialogGroup;
030import org.kuali.rice.krad.uif.container.Group;
031import org.kuali.rice.krad.uif.container.collections.LineBuilderContext;
032import org.kuali.rice.krad.uif.element.Action;
033import org.kuali.rice.krad.uif.element.Message;
034import org.kuali.rice.krad.uif.field.Field;
035import org.kuali.rice.krad.uif.layout.collections.CollectionLayoutManagerBase;
036import org.kuali.rice.krad.uif.layout.collections.CollectionPagingHelper;
037import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle;
038import org.kuali.rice.krad.uif.lifecycle.ViewLifecycleRestriction;
039import org.kuali.rice.krad.uif.lifecycle.ViewLifecycleUtils;
040import org.kuali.rice.krad.uif.util.ComponentFactory;
041import org.kuali.rice.krad.uif.util.ComponentUtils;
042import org.kuali.rice.krad.uif.util.ContextUtils;
043import org.kuali.rice.krad.uif.util.LifecycleElement;
044import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
045import org.kuali.rice.krad.uif.view.ExpressionEvaluator;
046import org.kuali.rice.krad.uif.view.View;
047import org.kuali.rice.krad.uif.view.ViewModel;
048import org.kuali.rice.krad.util.KRADUtils;
049import org.kuali.rice.krad.web.form.UifFormBase;
050
051import java.util.ArrayList;
052import java.util.HashMap;
053import java.util.List;
054import java.util.Map;
055
056/**
057 * Layout manager that works with {@code CollectionGroup} containers and
058 * renders the collection lines in a vertical row
059 *
060 * <p>
061 * For each line of the collection, a {@code Group} instance is created.
062 * The group header contains a label for the line (summary information), the
063 * group fields are the collection line fields, and the group footer contains
064 * the line actions. All the groups are rendered using the
065 * {@code BoxLayoutManager} with vertical orientation.
066 * </p>
067 *
068 * <p>
069 * Modify the lineGroupPrototype to change header/footer styles or any other
070 * customization for the line groups
071 * </p>
072 *
073 * @author Kuali Rice Team (rice.collab@kuali.org)
074 */
075@BeanTags({@BeanTag(name = "stackedCollectionLayout-bean", parent = "Uif-StackedCollectionLayoutBase"),
076        @BeanTag(name = "stackedCollectionLayout-withGridItems-bean",
077                parent = "Uif-StackedCollectionLayout-WithGridItems"),
078        @BeanTag(name = "stackedCollectionLayout-withBoxItems-bean",
079                parent = "Uif-StackedCollectionLayout-WithBoxItems"),
080        @BeanTag(name = "stackedCollectionLayout-list-bean", parent = "Uif-StackedCollectionLayout-List")})
081public class StackedLayoutManagerBase extends CollectionLayoutManagerBase implements StackedLayoutManager {
082    private static final long serialVersionUID = 4602368505430238846L;
083
084    @KeepExpression
085    private String summaryTitle;
086    private List<String> summaryFields;
087
088    private Group lineGroupPrototype;
089    private Group wrapperGroup;
090
091    private List<Group> stackedGroups;
092
093    private boolean renderLineActionsInLineGroup;
094    private boolean renderLineActionsInHeader;
095
096    public StackedLayoutManagerBase() {
097        super();
098
099        summaryFields = new ArrayList<String>();
100        stackedGroups = new ArrayList<Group>();
101    }
102
103    /**
104     * {@inheritDoc}
105     */
106    @Override
107    public void performInitialization(Object model) {
108        super.performInitialization(model);
109
110        stackedGroups = new ArrayList<Group>();
111    }
112
113    /**
114     * {@inheritDoc}
115     */
116    @Override
117    public void performApplyModel(Object model, LifecycleElement component) {
118        super.performApplyModel(model, component);
119
120        if (wrapperGroup != null) {
121            wrapperGroup.setItems(stackedGroups);
122        }
123    }
124
125    /**
126     * {@inheritDoc}
127     */
128    @Override
129    public void performFinalize(Object model, LifecycleElement element) {
130        super.performFinalize(model, element);
131
132        boolean serverPagingEnabled =
133                (element instanceof CollectionGroup) && ((CollectionGroup) element).isUseServerPaging();
134
135        // set the appropriate page, total pages, and link script into the Pager
136        if (serverPagingEnabled && this.getPagerWidget() != null) {
137            CollectionLayoutUtils.setupPagerWidget(getPagerWidget(), (CollectionGroup) element, model);
138        }
139    }
140
141    /**
142     * {@inheritDoc}
143     */
144    @Override
145    public void buildLine(LineBuilderContext lineBuilderContext) {
146        View view = ViewLifecycle.getView();
147
148        List<Field> lineFields = lineBuilderContext.getLineFields();
149        CollectionGroup collectionGroup = lineBuilderContext.getCollectionGroup();
150        int lineIndex = lineBuilderContext.getLineIndex();
151        String idSuffix = lineBuilderContext.getIdSuffix();
152        Object currentLine = lineBuilderContext.getCurrentLine();
153
154        String bindingPath = lineBuilderContext.getBindingPath();
155
156        Map<String, Object> lineContext = new HashMap<String, Object>();
157        lineContext.putAll(this.getContext());
158        lineContext.put(UifConstants.ContextVariableNames.LINE, currentLine);
159        lineContext.put(UifConstants.ContextVariableNames.MANAGER, this);
160        lineContext.put(UifConstants.ContextVariableNames.VIEW, view);
161        lineContext.put(UifConstants.ContextVariableNames.LINE_SUFFIX, idSuffix);
162        lineContext.put(UifConstants.ContextVariableNames.INDEX, Integer.valueOf(lineIndex));
163        lineContext.put(UifConstants.ContextVariableNames.COLLECTION_GROUP, collectionGroup);
164        lineContext.put(UifConstants.ContextVariableNames.IS_ADD_LINE, lineBuilderContext.isAddLine());
165        lineContext.put(UifConstants.ContextVariableNames.READONLY_LINE, Boolean.TRUE.equals(collectionGroup.getReadOnly()));
166        lineContext.put(UifConstants.ContextVariableNames.PARENT_LINE, currentLine);
167
168        ExpressionEvaluator expressionEvaluator = ViewLifecycle.getExpressionEvaluator();
169
170        // construct new group
171        Group lineGroup = null;
172        if (lineBuilderContext.isAddLine()) {
173            stackedGroups = new ArrayList<Group>();
174
175            if (getAddLineGroup() == null) {
176                lineGroup = ComponentUtils.copy(lineGroupPrototype, idSuffix);
177            } else {
178                lineGroup = ComponentUtils.copy(getAddLineGroup(), idSuffix);
179                lineGroup.addStyleClass(collectionGroup.getAddItemCssClass());
180            }
181
182            // add line enter key action
183            addEnterKeyDataAttributeToGroup(lineGroup, lineContext, expressionEvaluator,
184                    collectionGroup.getAddLineEnterKeyAction());
185        } else {
186            lineGroup = ComponentUtils.copy(lineGroupPrototype, idSuffix);
187
188            // existing line enter key action
189            addEnterKeyDataAttributeToGroup(lineGroup, lineContext, expressionEvaluator,
190                    collectionGroup.getLineEnterKeyAction());
191        }
192
193        if (((UifFormBase) lineBuilderContext.getModel()).isAddedCollectionItem(currentLine)) {
194            lineGroup.addStyleClass(collectionGroup.getNewItemsCssClass());
195        }
196
197        // any actions that are attached to the group prototype (like the header) need to get action parameters
198        // and context set for the collection line
199        List<Action> lineGroupActions = ViewLifecycleUtils.getElementsOfTypeDeep(lineGroup, Action.class);
200        if (lineGroupActions != null) {
201            collectionGroup.getCollectionGroupBuilder().initializeActions(lineGroupActions, collectionGroup, lineIndex);
202            ContextUtils.updateContextsForLine(lineGroupActions, collectionGroup, currentLine, lineIndex, idSuffix);
203        }
204
205        ContextUtils.updateContextForLine(lineGroup, collectionGroup, currentLine, lineIndex, idSuffix);
206
207        // build header for the group
208        if (lineBuilderContext.isAddLine()) {
209            if (lineGroup.getHeader() != null && StringUtils.isNotBlank(lineGroup.getHeaderText())) {
210                Message headerMessage = ComponentUtils.copy(collectionGroup.getAddLineLabel());
211                headerMessage.setMessageText(lineGroup.getHeaderText());
212            }
213        } else {
214            // get the collection for this group from the model
215            List<Object> modelCollection = ObjectPropertyUtils.getPropertyValue(lineBuilderContext.getModel(),
216                    ((DataBinding) collectionGroup).getBindingInfo().getBindingPath());
217
218            String headerText = buildLineHeaderText(modelCollection.get(lineIndex), lineGroup);
219
220            // don't set header if text is blank (could already be set by other means)
221            if (StringUtils.isNotBlank(headerText) && lineGroup.getHeader() != null) {
222                lineGroup.getHeader().setHeaderText(headerText);
223            }
224        }
225
226        // stack all fields (including sub-collections) for the group
227        List<Component> groupFields = new ArrayList<Component>();
228        groupFields.addAll(lineFields);
229
230        if (lineBuilderContext.getSubCollectionFields() != null) {
231            groupFields.addAll(lineBuilderContext.getSubCollectionFields());
232        }
233
234        // Place actions in the appropriate location for the stacked group line
235        determineLineActionPlacement(lineGroup, collectionGroup, lineBuilderContext, groupFields);
236
237        lineGroup.setItems(groupFields);
238
239        // add items to add line group
240        if (lineBuilderContext.isAddLine()) {
241            if (getAddLineGroup() != null) {
242                getAddLineGroup().setItems(lineGroup.getItems());
243            }
244        }
245
246        // Must evaluate the client-side state on the lineGroup's disclosure for PlaceholderDisclosureGroup processing
247        if (lineBuilderContext.getModel() instanceof ViewModel) {
248            KRADUtils.syncClientSideStateForComponent(lineGroup.getDisclosure(),
249                    ((ViewModel) lineBuilderContext.getModel()).getClientStateForSyncing());
250        }
251
252        // don't add to stackedGroups else will get double set of dialog boxes
253        // see FreeMarkerInlineRenderUtils.renderCollectionGroup near end where renders add line dialog
254        if (lineGroup instanceof DialogGroup == false) {
255            stackedGroups.add(lineGroup);
256        }
257
258        // we need to add the parent line for each of the items in the group
259        ContextUtils.pushObjectToContextDeep(lineGroup.getItems(), UifConstants.ContextVariableNames.PARENT_LINE,
260                lineBuilderContext.getCurrentLine());
261    }
262
263    /**
264     * Places actions in the appropriate location for the stacked group line based on placement
265     * flags set on this layout manager
266     *
267     * @param lineGroup the current line group
268     * @param collectionGroup the current collection group
269     * @param lineBuilderContext the line's building context
270     * @param groupFields the list of fields which will be added to the line group
271     */
272    protected void determineLineActionPlacement(Group lineGroup, CollectionGroup collectionGroup,
273            LineBuilderContext lineBuilderContext, List<Component> groupFields) {
274        List<? extends Component> actions = lineBuilderContext.getLineActions();
275
276        boolean showActions = collectionGroup.isRenderLineActions() && !Boolean.TRUE.equals(collectionGroup.getReadOnly());
277        if (!showActions)  {
278            return;
279        }
280
281        if (renderLineActionsInHeader && lineGroup.getHeader() != null && !lineBuilderContext.isAddLine()) {
282            // add line actions to header when the option is true
283            Group headerGroup = lineGroup.getHeader().getRightGroup();
284
285            if (headerGroup == null) {
286                headerGroup = ComponentFactory.getHorizontalBoxGroup();
287            }
288
289            List<Component> items = new ArrayList<Component>();
290            items.addAll(headerGroup.getItems());
291            items.addAll(actions);
292
293            headerGroup.setItems(items);
294            lineGroup.getHeader().setRightGroup(headerGroup);
295        } else if (isRenderLineActionsInLineGroup()) {
296            // add the actions to the line group if isRenderLineActionsInLineGroup flag is true
297            groupFields.addAll(actions);
298            lineGroup.setRenderFooter(false);
299        } else if ((lineGroup.getFooter() != null) && ((lineGroup.getFooter().getItems() == null) || lineGroup
300                .getFooter().getItems().isEmpty())) {
301            // add to footer in the default case
302            lineGroup.getFooter().setItems(actions);
303        }
304    }
305
306    /**
307     * Builds the header text for the collection line
308     *
309     * <p>
310     * Header text is built up by first the collection label, either specified
311     * on the collection definition or retrieved from the dictionary. Then for
312     * each summary field defined, the value from the model is retrieved and
313     * added to the header.
314     * </p>
315     *
316     * <p>
317     * Note the {@link #getSummaryTitle()} field may have expressions defined, in which cause it will be copied to the
318     * property expressions map to set the title for the line group (which will have the item context variable set)
319     * </p>
320     *
321     * @param line Collection line containing data
322     * @param lineGroup Group instance for rendering the line and whose title should be built
323     * @return header text for line
324     */
325    protected String buildLineHeaderText(Object line, Group lineGroup) {
326        // check for expression on summary title
327        if (ViewLifecycle.getExpressionEvaluator().containsElPlaceholder(summaryTitle)) {
328            lineGroup.getPropertyExpressions().put(UifPropertyPaths.HEADER_TEXT, summaryTitle);
329            return null;
330        }
331
332        // build up line summary from declared field values and fixed title
333        String summaryFieldString = "";
334        for (String summaryField : summaryFields) {
335            Object summaryFieldValue = ObjectPropertyUtils.getPropertyValue(line, summaryField);
336            if (StringUtils.isNotBlank(summaryFieldString)) {
337                summaryFieldString += " - ";
338            }
339
340            if (summaryFieldValue != null) {
341                summaryFieldString += summaryFieldValue;
342            } else {
343                summaryFieldString += "Null";
344            }
345        }
346
347        String headerText = summaryTitle;
348        if (StringUtils.isNotBlank(summaryFieldString)) {
349            headerText += " ( " + summaryFieldString + " )";
350        }
351
352        return headerText;
353    }
354
355    /**
356     * Invokes {@link org.kuali.rice.krad.uif.layout.collections.CollectionPagingHelper} to carry out the
357     * paging request.
358     *
359     * {@inheritDoc}
360     */
361    @Override
362    public void processPagingRequest(Object model, CollectionGroup collectionGroup) {
363        String pageNumber = ViewLifecycle.getRequest().getParameter(UifConstants.PageRequest.PAGE_NUMBER);
364
365        CollectionPagingHelper pagingHelper = new CollectionPagingHelper();
366        pagingHelper.processPagingRequest(ViewLifecycle.getView(), collectionGroup, (UifFormBase) model, pageNumber);
367    }
368
369    /**
370     * Returns the parent {@link org.kuali.rice.krad.uif.layout.collections.CollectionLayoutManagerBase}'s add line group
371     *
372     * <p>
373     * This method is overridden to restrict the lifecycle of the add line group as a resolution to avoid duplicate
374     * components from being added to the view, for example, quickfinders.
375     * </p>
376     *
377     * {@inheritDoc}
378     */
379    @Override
380    @BeanTagAttribute
381    @ViewLifecycleRestriction(UifConstants.ViewPhases.INITIALIZE)
382    public Group getAddLineGroup() {
383        return super.getAddLineGroup();
384    }
385
386    /**
387     * {@inheritDoc}
388     */
389    @Override
390    public Class<? extends Container> getSupportedContainer() {
391        return CollectionGroup.class;
392    }
393
394    /**
395     * {@inheritDoc}
396     */
397    @Override
398    @BeanTagAttribute
399    public String getSummaryTitle() {
400        return this.summaryTitle;
401    }
402
403    /**
404     * {@inheritDoc}
405     */
406    @Override
407    public void setSummaryTitle(String summaryTitle) {
408        this.summaryTitle = summaryTitle;
409    }
410
411    /**
412     * {@inheritDoc}
413     */
414    @Override
415    @BeanTagAttribute
416    public List<String> getSummaryFields() {
417        return this.summaryFields;
418    }
419
420    /**
421     * {@inheritDoc}
422     */
423    @Override
424    public void setSummaryFields(List<String> summaryFields) {
425        this.summaryFields = summaryFields;
426    }
427
428    /**
429     * {@inheritDoc}
430     */
431    @Override
432    @ViewLifecycleRestriction(UifConstants.ViewPhases.INITIALIZE)
433    @BeanTagAttribute
434    public Group getLineGroupPrototype() {
435        return this.lineGroupPrototype;
436    }
437
438    /**
439     * {@inheritDoc}
440     */
441    @Override
442    public void setLineGroupPrototype(Group lineGroupPrototype) {
443        this.lineGroupPrototype = lineGroupPrototype;
444    }
445
446    /**
447     * {@inheritDoc}
448     */
449    @Override
450    @BeanTagAttribute
451    public Group getWrapperGroup() {
452        return wrapperGroup;
453    }
454
455    /**
456     * {@inheritDoc}
457     */
458    @Override
459    public void setWrapperGroup(Group wrapperGroup) {
460        this.wrapperGroup = wrapperGroup;
461    }
462
463    /**
464     * {@inheritDoc}
465     */
466    @Override
467    @ViewLifecycleRestriction
468    @BeanTagAttribute
469    public List<Group> getStackedGroups() {
470        return this.stackedGroups;
471    }
472
473    /**
474     * {@inheritDoc}
475     */
476    @Override
477    public List<Group> getStackedGroupsNoWrapper() {
478        return wrapperGroup != null ? null : this.stackedGroups;
479    }
480
481    /**
482     * {@inheritDoc}
483     */
484    @Override
485    public void setStackedGroups(List<Group> stackedGroups) {
486        this.stackedGroups = stackedGroups;
487    }
488
489    /**
490     * {@inheritDoc}
491     */
492    @Override
493    @BeanTagAttribute
494    public boolean isRenderLineActionsInLineGroup() {
495        return renderLineActionsInLineGroup;
496    }
497
498    /**
499     * {@inheritDoc}
500     */
501    @Override
502    public void setRenderLineActionsInLineGroup(boolean renderLineActionsInLineGroup) {
503        this.renderLineActionsInLineGroup = renderLineActionsInLineGroup;
504    }
505
506    /**
507     * {@inheritDoc}
508     */
509    @Override
510    @BeanTagAttribute
511    public boolean isRenderLineActionsInHeader() {
512        return renderLineActionsInHeader;
513    }
514
515    /**
516     * {@inheritDoc}
517     */
518    @Override
519    public void setRenderLineActionsInHeader(boolean renderLineActionsInHeader) {
520        this.renderLineActionsInHeader = renderLineActionsInHeader;
521    }
522}