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.util;
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.datadictionary.uif.UifDictionaryBeanBase;
023import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
024import org.kuali.rice.krad.uif.UifConstants;
025import org.kuali.rice.krad.uif.component.Component;
026import org.kuali.rice.krad.uif.container.PageGroup;
027import org.kuali.rice.krad.uif.element.BreadcrumbItem;
028import org.kuali.rice.krad.uif.element.Header;
029import org.kuali.rice.krad.uif.lifecycle.ViewLifecycle;
030import org.kuali.rice.krad.uif.view.ExpressionEvaluator;
031import org.kuali.rice.krad.uif.view.View;
032
033import java.io.Serializable;
034import java.util.ArrayList;
035import java.util.List;
036import java.util.Map;
037
038/**
039 * ParentLocation is used to provide automatic generation/determination of Views/Pages that occur before the current
040 * View.  Essentially, this class provides a way to determine a conceptual hierarchy of view/page locations.
041 * This information is used internally to generate BreadcrumbItems that can appear before the View's breadcrumbs.
042 */
043@BeanTag(name = "parentLocation", parent = "Uif-ParentLocation")
044public class ParentLocation extends UifDictionaryBeanBase implements Serializable {
045
046    private static final long serialVersionUID = -6242148809697931126L;
047    //private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(ParentLocation.class);
048
049    private UrlInfo parentViewUrl;
050    private UrlInfo parentPageUrl;
051    private String parentViewLabel;
052    private String parentPageLabel;
053
054    private BreadcrumbItem viewBreadcrumbItem;
055    private BreadcrumbItem pageBreadcrumbItem;
056    protected List<BreadcrumbItem> resolvedBreadcrumbItems = new ArrayList<BreadcrumbItem>();
057
058    /**
059     * Construct the parent location breadcrumbItems that represent all the parent views/pages configured through
060     * parentLocation by traversing through each view by id referenced in parentViewUrl in a chain recursively.  A url
061     * which is not using viewId and instead set the href explicitly ends the chain.
062     *
063     * @param view the current view being processed
064     * @param currentModel the currentModel
065     * @param currentContext the currentContext
066     * @return list of breadcrumbItems (the final list is set into the top most View's
067     *         parentLocation.resolvedBreadcrumbItems)
068     */
069    public List<BreadcrumbItem> constructParentLocationBreadcrumbItems(View view, Object currentModel,
070            Map<String, Object> currentContext) {
071        //viewBreadcrumbItem must already have an object initialized
072        if (viewBreadcrumbItem == null) {
073            return resolvedBreadcrumbItems;
074        }
075
076        //evaluate expressions on relevant content before comparisons
077        this.handleExpressions(view, currentModel, currentContext, ViewLifecycle.getExpressionEvaluator());
078
079        //set url values into breadcrumb objects
080        if (StringUtils.isNotBlank(parentViewUrl.getOriginalHref()) || (StringUtils.isNotBlank(
081                parentViewUrl.getViewId()) && StringUtils.isNotBlank(parentViewUrl.getControllerMapping()))) {
082            viewBreadcrumbItem.setUrl(parentViewUrl);
083            viewBreadcrumbItem.setLabel(parentViewLabel);
084        }
085
086        if (StringUtils.isNotBlank(parentPageUrl.getOriginalHref()) || (StringUtils.isNotBlank(
087                parentPageUrl.getViewId()) && StringUtils.isNotBlank(parentPageUrl.getControllerMapping()))) {
088            pageBreadcrumbItem.setUrl(parentPageUrl);
089            pageBreadcrumbItem.setLabel(parentPageLabel);
090        }
091
092        //only continue if either href or viewId are explicitly set (check for validity of parent url)
093        if (viewBreadcrumbItem.getUrl() == null || StringUtils.isBlank(viewBreadcrumbItem.getUrl().getOriginalHref())
094                && StringUtils.isBlank(viewBreadcrumbItem.getUrl().getViewId())) {
095            return resolvedBreadcrumbItems;
096        }
097
098        String parentViewId = viewBreadcrumbItem.getUrl().getViewId();
099        String controllerMapping = viewBreadcrumbItem.getUrl().getControllerMapping();
100
101        View parentView = null;
102        //chaining is only allowed when the controllerMapping and viewId are explicitly set
103        if (viewBreadcrumbItem.getUrl() != null && StringUtils.isNotBlank(controllerMapping) && StringUtils.isNotBlank(
104                parentViewId) && StringUtils.isBlank(viewBreadcrumbItem.getUrl().getOriginalHref())) {
105            parentView = KRADServiceLocatorWeb.getDataDictionaryService().getViewById(parentViewId);
106        }
107
108        //only do this processing if the parentView is not null (viewId was set on viewBreadcrumbItem to a valid View)
109        if (parentView != null) {
110            processParentViewDerivedContent(parentView, parentViewId, view, currentModel, currentContext);
111        }
112
113        //add parent view breadcrumb
114        if (StringUtils.isNotEmpty(viewBreadcrumbItem.getLabel())) {
115            resolvedBreadcrumbItems.add(viewBreadcrumbItem);
116        }
117
118        //add parent page breadcrumb
119        if (pageBreadcrumbItem != null && StringUtils.isNotEmpty(pageBreadcrumbItem.getLabel())) {
120            resolvedBreadcrumbItems.add(pageBreadcrumbItem);
121        }
122
123        return resolvedBreadcrumbItems;
124    }
125
126    /**
127     * Processes content that can only be derived by looking at the parentView for a parentLocation, such as
128     * expressions
129     * and sibling breadcrumb content; evaluates and adds them to the ParentLocation BreadcrumbItem(s).
130     *
131     * @param parentView the parentView to derive breadcrumb content from
132     * @param parentViewId the parentView's id
133     * @param currentView the currentView (the view this parentLocation is on)
134     * @param currentModel the current model data
135     * @param currentContext the current context to evaluate expressions against
136     */
137    private void processParentViewDerivedContent(View parentView, String parentViewId, View currentView,
138            Object currentModel, Map<String, Object> currentContext) {
139        //populate expression graph
140        ViewLifecycle.getExpressionEvaluator().populatePropertyExpressionsFromGraph(parentView, false);
141
142        //chain parent locations if not null on parent
143        if (((View) parentView).getParentLocation() != null) {
144            resolvedBreadcrumbItems.addAll(
145                    ((View) parentView).getParentLocation().constructParentLocationBreadcrumbItems(parentView,
146                            currentModel, currentContext));
147        }
148
149        handleLabelExpressions(parentView, currentModel, currentContext, ViewLifecycle.getExpressionEvaluator());
150
151        //label automation, if parent has a label for its breadcrumb and one is not set here use that value
152        //it is assumed that if the label contains a SpringEL expression, those properties are available on the
153        //current form by the same name
154        if (StringUtils.isBlank(viewBreadcrumbItem.getLabel()) && parentView.getBreadcrumbItem() != null &&
155                StringUtils.isNotBlank(parentView.getBreadcrumbItem().getLabel())) {
156            viewBreadcrumbItem.setLabel(parentView.getBreadcrumbItem().getLabel());
157        } else if (StringUtils.isBlank(viewBreadcrumbItem.getLabel()) && StringUtils.isNotBlank(
158                parentView.getHeaderText())) {
159            viewBreadcrumbItem.setLabel(parentView.getHeaderText());
160        }
161
162        //siblingBreadcrumb inheritance automation
163        if (parentView.getBreadcrumbItem() != null
164                && parentView.getBreadcrumbItem().getSiblingBreadcrumbComponent() != null
165                && viewBreadcrumbItem.getSiblingBreadcrumbComponent() == null) {
166            viewBreadcrumbItem.setSiblingBreadcrumbComponent(
167                    parentView.getBreadcrumbItem().getSiblingBreadcrumbComponent());
168        }
169
170        //page breadcrumb label automation, page must be a page of the view breadcrumb
171        if (pageBreadcrumbItem != null && StringUtils.isNotBlank(pageBreadcrumbItem.getUrl().getPageId()) && StringUtils
172                .isNotBlank(pageBreadcrumbItem.getUrl().getViewId()) && pageBreadcrumbItem.getUrl().getViewId().equals(
173                parentViewId)) {
174            handlePageBreadcrumb(parentView, currentModel);
175        }
176    }
177
178    /**
179     * Evaluates the expressions on properties that may be determine the value of the label used on generated view and
180     * page breadcrumbItems (if a label was not explicitly set)
181     *
182     * @param parentView the parentView
183     * @param currentModel the currentModel
184     * @param currentContext the currentContext
185     * @param expressionEvaluator instance of expression evaluator for the current view
186     */
187    private void handleLabelExpressions(View parentView, Object currentModel, Map<String, Object> currentContext,
188            ExpressionEvaluator expressionEvaluator) {
189        try {
190            Header header = parentView.getHeader();
191
192            if (header != null) {
193                if (StringUtils.isNotBlank(parentView.getPropertyExpressions().get(
194                        UifConstants.ComponentProperties.HEADER_TEXT))) {
195                    header.getPropertyExpressions().put(UifConstants.ComponentProperties.HEADER_TEXT,
196                            parentView.getPropertyExpressions().get(UifConstants.ComponentProperties.HEADER_TEXT));
197                }
198
199                expressionEvaluator.evaluateExpressionsOnConfigurable(parentView, header, currentContext);
200            }
201
202            BreadcrumbItem breadcrumbItem = parentView.getBreadcrumbItem();
203
204            if (breadcrumbItem != null) {
205                expressionEvaluator.evaluateExpressionsOnConfigurable(parentView, breadcrumbItem, currentContext);
206            }
207
208            if (pageBreadcrumbItem != null && pageBreadcrumbItem.getUrl() != null && StringUtils.isNotBlank(
209                    pageBreadcrumbItem.getUrl().getPageId())) {
210                PageGroup thePage = null;
211                if (parentView.isSinglePageView() && parentView.getPage() != null) {
212                    thePage = parentView.getPage();
213                } else {
214                    for (Component item : parentView.getItems()) {
215                        if (item.getId().equals(pageBreadcrumbItem.getUrl().getPageId())) {
216                            thePage = (PageGroup) item;
217                            break;
218                        }
219                    }
220                }
221
222                if (thePage == null) {
223                    //TODO throw error
224                    return;
225                }
226
227                //populate from expression graph
228                ViewLifecycle.getExpressionEvaluator().populatePropertyExpressionsFromGraph(thePage, false);
229
230                Header pageHeader = thePage.getHeader();
231
232                if (pageHeader != null) {
233                    if (StringUtils.isNotBlank(thePage.getPropertyExpressions().get(
234                            UifConstants.ComponentProperties.HEADER_TEXT))) {
235                        pageHeader.getPropertyExpressions().put(UifConstants.ComponentProperties.HEADER_TEXT,
236                                thePage.getPropertyExpressions().get(UifConstants.ComponentProperties.HEADER_TEXT));
237                    }
238
239                    expressionEvaluator.evaluateExpressionsOnConfigurable(parentView, pageHeader, currentContext);
240                }
241
242                BreadcrumbItem pageBreadcrumb = thePage.getBreadcrumbItem();
243
244                if (pageBreadcrumb != null) {
245                    expressionEvaluator.evaluateExpressionsOnConfigurable(parentView, pageBreadcrumb, currentContext);
246                }
247            }
248        } catch (RuntimeException e) {
249            throw new RuntimeException("There was likely a problem evaluating an expression in a parent view or page"
250                    + " because a property may not exist in the current context - explicitly set the label for this"
251                    + " parentLocation: "
252                    + parentView.getId(), e);
253        }
254    }
255
256    /**
257     * Evaluate any expressions that may have not been evaluated for the urls and breadcrumbItems of this
258     * parentLocation
259     * class using the currentModel and currentContext
260     *
261     * @param view the view
262     * @param currentModel the current model
263     * @param currentContext the current context
264     * @param expressionEvaluator instance of expression evaluator for the current view
265     */
266    private void handleExpressions(View view, Object currentModel, Map<String, Object> currentContext,
267            ExpressionEvaluator expressionEvaluator) {
268        try {
269            // KULRICE-10053 initialize the expression evaluator
270            expressionEvaluator.initializeEvaluationContext(currentModel);
271
272            //Evaluate view url/breadcrumb expressions
273            expressionEvaluator.evaluateExpressionsOnConfigurable(view, viewBreadcrumbItem, currentContext);
274
275            if (viewBreadcrumbItem.getUrl() != null) {
276                expressionEvaluator.evaluateExpressionsOnConfigurable(view, viewBreadcrumbItem.getUrl(),
277                        currentContext);
278            }
279
280            if (parentViewUrl != null) {
281                expressionEvaluator.evaluateExpressionsOnConfigurable(view, parentViewUrl, currentContext);
282            }
283
284            //evaluate same for potential page properties
285            if (pageBreadcrumbItem != null) {
286                expressionEvaluator.evaluateExpressionsOnConfigurable(view, pageBreadcrumbItem, currentContext);
287
288                if (pageBreadcrumbItem.getUrl() != null) {
289                    expressionEvaluator.evaluateExpressionsOnConfigurable(view, pageBreadcrumbItem.getUrl(),
290                            currentContext);
291                }
292            }
293
294            if (parentPageUrl != null) {
295                expressionEvaluator.evaluateExpressionsOnConfigurable(view, parentPageUrl, currentContext);
296            }
297        } catch (RuntimeException e) {
298            throw new RuntimeException("There was likely a problem evaluating an expression in a parent view or page"
299                    + " because a property may not exist in the current context - problem in Url or BreadcrumbItem"
300                    + " - set these to something that can be evaluated - of the parentLocation: "
301                    + view.getId(), e);
302        }
303    }
304
305    /**
306     * Handle setting a page breadcrumbItem label when parentPageUrl is being used based on the PageGroup's
307     * breadcrumbItem and header properties
308     *
309     * @param view the current view
310     */
311    private void handlePageBreadcrumb(View view, Object currentModel) {
312        PageGroup thePage = null;
313        if (view.isSinglePageView() && view.getPage() != null) {
314            thePage = view.getPage();
315        } else {
316            for (Component item : view.getItems()) {
317                if (item.getId().equals(pageBreadcrumbItem.getUrl().getPageId())) {
318                    thePage = (PageGroup) item;
319                    break;
320                }
321            }
322        }
323
324        if (thePage == null) {
325            return;
326        }
327
328        //set label
329        if (StringUtils.isBlank(pageBreadcrumbItem.getLabel()) && thePage.getBreadcrumbItem() != null &&
330                StringUtils.isNotBlank(thePage.getBreadcrumbItem().getLabel())) {
331            pageBreadcrumbItem.setLabel(thePage.getBreadcrumbItem().getLabel());
332        } else if (StringUtils.isBlank(pageBreadcrumbItem.getLabel()) && StringUtils.isNotBlank(
333                thePage.getHeaderText())) {
334            pageBreadcrumbItem.setLabel(thePage.getHeaderText());
335        }
336
337        //page siblingBreadcrumb inheritance automation
338        if (thePage.getBreadcrumbItem() != null
339                && thePage.getBreadcrumbItem().getSiblingBreadcrumbComponent() != null
340                && pageBreadcrumbItem.getSiblingBreadcrumbComponent() == null) {
341            pageBreadcrumbItem.setSiblingBreadcrumbComponent(
342                    thePage.getBreadcrumbItem().getSiblingBreadcrumbComponent());
343        }
344    }
345
346    /**
347     * The parentViewUrl representing the url that is the parent of this View.
348     *
349     * <p>
350     * This url can explicitly set an href
351     * or can set a controller and viewId.  Parent view traversal is only performed if the controller and viewId
352     * properties are set and NOT the explicit href (this affects if breadcrumbs are generated in a recursive chain).
353     * </p>
354     *
355     * @return the parent view url
356     */
357    @BeanTagAttribute(name = "parentViewUrl", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
358    public UrlInfo getParentViewUrl() {
359        return parentViewUrl;
360    }
361
362    /**
363     * Set the parentViewUrl
364     *
365     * @param parentViewUrl
366     */
367    public void setParentViewUrl(UrlInfo parentViewUrl) {
368        this.parentViewUrl = parentViewUrl;
369    }
370
371    /**
372     * The parentPageUrl representing a page url that is the parent of this View.  In order for automated label
373     * determination to work for the page breadcrumbItem, the viewId and controllerMapping must match with the
374     * parentViewUrl.
375     *
376     * <p>
377     * This url can explicitly set an href or can set a pageId.  The parentViewUrl MUST be set before this option can
378     * be set.  If the needed behavior is such that the parent view breadcrumbItem should not be shown and only this
379     * item should be shown, set 'parentLocation.viewBreadcrumbItem.render' to false.
380     * </p>
381     *
382     * @return the parent page url
383     */
384    @BeanTagAttribute(name = "parentPageUrl", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
385    public UrlInfo getParentPageUrl() {
386        return parentPageUrl;
387    }
388
389    /**
390     * Set the parentPageUrl
391     *
392     * @param parentPageUrl
393     */
394    public void setParentPageUrl(UrlInfo parentPageUrl) {
395        this.parentPageUrl = parentPageUrl;
396    }
397
398    /**
399     * The parentViewLabel is the text used for breadcrumbItem label of the parent view.
400     *
401     * <p>
402     * If not set, the the label is determined
403     * by looking at the parent View's breadcrumbItem and then its headerText.  If the parent view's retrieved value
404     * contain expressions, those expressions must be able to be evaluated in the current context (ie, the properties
405     * they reference must also exist on the current form at the same location) or an exception will be thrown.
406     * </p>
407     *
408     * @return the parentViewLabel set
409     */
410    @BeanTagAttribute(name = "parentViewLabel")
411    public String getParentViewLabel() {
412        return parentViewLabel;
413    }
414
415    /**
416     * Set the parentViewLabel
417     *
418     * @param parentViewLabel
419     */
420    public void setParentViewLabel(String parentViewLabel) {
421        this.parentViewLabel = parentViewLabel;
422    }
423
424    /**
425     * The parentPageLabel is the text used for breadcrumbItem label of the parent page.
426     *
427     * <p>
428     * If not set, the the label is determined
429     * by looking at the parent PageGroup's breadcrumbItem and then its headerText.  This retrieval can only happen
430     * if the parentViewUrl is set.
431     * If the parent PageGroup's retrieved value
432     * contain expressions, those expressions must be able to be evaluated in the current context (ie, the properties
433     * they reference must also exist on the current form at the same location) or an exception will be thrown.
434     * </p>
435     *
436     * @return the parentPageLabel set
437     */
438    @BeanTagAttribute(name = "parentPageLabel")
439    public String getParentPageLabel() {
440        return parentPageLabel;
441    }
442
443    /**
444     * Set the parentPageLabel
445     *
446     * @param parentPageLabel
447     */
448    public void setParentPageLabel(String parentPageLabel) {
449        this.parentPageLabel = parentPageLabel;
450    }
451
452    /**
453     * The viewBreadcrumbItem to use for the parent location view breadcrumb.  Url should NOT be set here because
454     * parentViewUrl is ALWAYS set into this breadcrumbItem, regardless of value.
455     *
456     * @return the viewBreadcrumbItem
457     */
458    @BeanTagAttribute(name = "viewBreadcrumbItem", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
459    public BreadcrumbItem getViewBreadcrumbItem() {
460        return viewBreadcrumbItem;
461    }
462
463    /**
464     * Set the viewBreadcrumbItem
465     *
466     * @param breadcrumbItem
467     */
468    public void setViewBreadcrumbItem(BreadcrumbItem breadcrumbItem) {
469        this.viewBreadcrumbItem = breadcrumbItem;
470    }
471
472    /**
473     * The pageBreadcrumbItem to use for the parent location view breadcrumb.  Url should NOT be set here because
474     * parentPageUrl is ALWAYS set into this breadcrumbItem, regardless of value.
475     *
476     * @return the pageBreadcrumbItem
477     */
478    @BeanTagAttribute(name = "pageBreadcrumbItem", type = BeanTagAttribute.AttributeType.SINGLEBEAN)
479    public BreadcrumbItem getPageBreadcrumbItem() {
480        return pageBreadcrumbItem;
481    }
482
483    /**
484     * Set the pageBreadcrumbItem
485     *
486     * @param pageBreadcrumbItem
487     */
488    public void setPageBreadcrumbItem(BreadcrumbItem pageBreadcrumbItem) {
489        this.pageBreadcrumbItem = pageBreadcrumbItem;
490    }
491
492    /**
493     * The resolved/generated breadcrumbItems determined by traversing the parentLocation chain.  These cannot be set
494     * and must be generated by calling constructParentLocationBreadcrumbItems.
495     *
496     * @return the resolved breadcrumbItem list
497     */
498    public List<BreadcrumbItem> getResolvedBreadcrumbItems() {
499        return resolvedBreadcrumbItems;
500    }
501
502}