001    /**
002     * Copyright 2005-2012 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     */
016    package org.kuali.rice.krad.uif.view;
017    
018    import org.apache.commons.lang.StringUtils;
019    import org.apache.log4j.Logger;
020    import org.kuali.rice.krad.datadictionary.parse.BeanTag;
021    import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute;
022    import org.kuali.rice.krad.service.DataObjectMetaDataService;
023    import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
024    import org.kuali.rice.krad.uif.UifConstants;
025    import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
026    import org.kuali.rice.krad.uif.util.ViewModelUtils;
027    import org.kuali.rice.krad.web.form.UifFormBase;
028    
029    import javax.servlet.http.HttpServletRequest;
030    import java.io.Serializable;
031    import java.io.UnsupportedEncodingException;
032    import java.net.URLDecoder;
033    import java.net.URLEncoder;
034    import java.util.ArrayList;
035    import java.util.Enumeration;
036    import java.util.List;
037    
038    /**
039     * History class used to keep track of views visited so they can be displayed in the ui
040     * as breadcrumbs - both as homeward path and history path interpretations
041     *
042     * @author Kuali Rice Team (rice.collab@kuali.org)
043     */
044    @BeanTag(name="history")
045    public class History implements Serializable {
046        private static final long serialVersionUID = -8279297694371557335L;
047        private static final Logger LOG = Logger.getLogger(History.class);
048    
049        public static final String ENTRY_TOKEN = "$^$";
050        public static final String VAR_TOKEN = "^$^";
051    
052        private boolean appendHomewardPath;
053        private boolean appendPassedHistory;
054    
055        private HistoryEntry current;
056    
057        private List<HistoryEntry> homewardPath;
058        private List<HistoryEntry> historyEntries;
059    
060        public History() {
061            historyEntries = new ArrayList<HistoryEntry>();
062        }
063    
064        /**
065         * Generates a list of HistoryEntries that can be used as breadcrumbs by the breadcrumb widget.  This
066         * method appends the appropriate history information on the HistoryEntry url variables so when a view is requested
067         * its history can be regenerated for use in its breadcrumbs.  It also sets the the passed showHome variable to
068         * false to prevent showing the homeward path more than once (as it is passed through the history
069         * variable backwards). This does not include the current HistoryEntry as a breadcrumb but adds the formKey as the
070         * LAST_FORM_KEY to assist with server side form cleanup.
071         *
072         * @return
073         */
074        public List<HistoryEntry> getGeneratedBreadcrumbs() {
075            List<HistoryEntry> breadcrumbs = new ArrayList<HistoryEntry>();
076            for (int i = 0; i < historyEntries.size(); i++) {
077                if (i == 0) {
078                    breadcrumbs.add(copyEntry(historyEntries.get(i)));
079                } else {
080                    HistoryEntry breadcrumb = copyEntry(historyEntries.get(i));
081    
082                    String historyParam = "";
083                    for (int j = 0; j < i; j++) {
084                        historyParam = historyParam + ENTRY_TOKEN + historyEntries.get(j).toParam();
085                    }
086                    historyParam = historyParam.replaceFirst("\\" + ENTRY_TOKEN, "");
087    
088                    try {
089                        historyParam = URLEncoder.encode(historyParam, "UTF-8");
090                    } catch (Exception e) {
091                        throw new RuntimeException("Error encoding history param", e);
092                    }
093    
094                    if (StringUtils.isEmpty(breadcrumb.getUrl())) {
095                        continue;
096                    }
097    
098                    String url = breadcrumb.getUrl();
099                    if (url.contains("?")) {
100                        url += "&";
101                    } else {
102                        url += "?";
103                    }
104    
105                    url += UifConstants.UrlParams.HISTORY
106                            + "="
107                            + historyParam
108                            + "&"
109                            + UifConstants.UrlParams.LAST_FORM_KEY
110                            + "="
111                            + current.getFormKey();
112    
113                    breadcrumb.setUrl(url);
114                    breadcrumbs.add(breadcrumb);
115                }
116            }
117    
118            return breadcrumbs;
119        }
120    
121        /**
122         * Gets the current HistoryEntry in the breadcrumb format described in getGeneratedBreadcrumbs
123         */
124        public HistoryEntry getGeneratedCurrentBreadcrumb() {
125            if (current == null) {
126                return new HistoryEntry();
127            }
128    
129            HistoryEntry breadcrumb = copyEntry(current);
130            String historyParam = "";
131            for (int j = 0; j < historyEntries.size(); j++) {
132                historyParam = historyParam + ENTRY_TOKEN + historyEntries.get(j).toParam();
133            }
134            historyParam = historyParam.replaceFirst("\\" + ENTRY_TOKEN, "");
135    
136            try {
137                historyParam = URLEncoder.encode(historyParam, "UTF-8");
138            } catch (Exception e) {
139                throw new RuntimeException("Error encoding history param", e);
140            }
141    
142            String url = "";
143            if (breadcrumb.getUrl().contains("?")) {
144                url = breadcrumb.getUrl() + "&" + UifConstants.UrlParams.HISTORY + "=" + historyParam;
145            } else {
146                url = breadcrumb.getUrl() + "?" + UifConstants.UrlParams.HISTORY + "=" + historyParam;
147            }
148            breadcrumb.setUrl(url);
149    
150            return breadcrumb;
151        }
152    
153        /**
154         * Copies a HistoryEntry, for use during breadcrumb generation.
155         *
156         * @param e
157         * @return
158         */
159        private HistoryEntry copyEntry(HistoryEntry e) {
160            return new HistoryEntry(e.getViewId(), e.getPageId(), e.getTitle(), e.getUrl(), e.getFormKey());
161        }
162    
163        /**
164         * Pushes the information passed in to history.
165         * Note: currently only used internally in the class - be cautious about its external use.
166         *
167         * @param viewId
168         * @param pageId
169         * @param title
170         * @param url
171         * @param formKey
172         */
173        public void pushToHistory(String viewId, String pageId, String title, String url, String formKey) {
174            HistoryEntry entry = new HistoryEntry(viewId, pageId, title, url, formKey);
175            historyEntries.add(entry);
176        }
177    
178        /**
179         * Sets the current HistoryEntry using information from the form and the request.  This history parameter is
180         * extracted out of the url in order for a "clean" url to be used in history parameter and
181         * breadcrumb generation, as passing history history through the nested urls is unnecessary.
182         *
183         * @param form
184         * @param request
185         */
186        public void buildCurrentEntryFromRequest(UifFormBase form, HttpServletRequest request) {
187            boolean showHomeValue = false;
188            boolean pageIdValue = false;
189            boolean formKeyValue = false;
190    
191            String queryString = "";
192            String url = request.getRequestURL().toString();
193    
194            // remove history attribute
195            Enumeration<String> params = request.getParameterNames();
196            while (params.hasMoreElements()) {
197                String key = params.nextElement();
198                if (!key.equals(UifConstants.UrlParams.HISTORY)) {
199                    for (String value : request.getParameterValues(key)) {
200                        try {
201                            queryString = queryString + "&" + key + "=" + URLEncoder.encode(value, "UTF-8");
202                        } catch (UnsupportedEncodingException e) {
203                            throw new RuntimeException("Unable to encode parameter value", e);
204                        }
205                    }
206                }
207    
208                if (key.equals(UifConstants.UrlParams.PAGE_ID)) {
209                    pageIdValue = true;
210                } else if (key.equals(UifConstants.UrlParams.SHOW_HOME)) {
211                    showHomeValue = true;
212                } else if (key.equals(UifConstants.UrlParams.FORM_KEY)) {
213                    formKeyValue = true;
214                }
215            }
216    
217            // add formKey and pageId to url
218            if (StringUtils.isNotBlank(form.getFormKey()) && !formKeyValue) {
219                try {
220                    queryString = queryString + "&" + UifConstants.UrlParams.FORM_KEY + "=" + URLEncoder.encode(
221                            form.getFormKey(), "UTF-8");
222                } catch (UnsupportedEncodingException e) {
223                    throw new RuntimeException("Unable to encode form key", e);
224                }
225            }
226    
227            if (StringUtils.isNotBlank(form.getPageId()) && !pageIdValue) {
228                queryString = queryString + "&" + UifConstants.UrlParams.PAGE_ID + "=" + form.getPageId();
229            }
230    
231            if (!showHomeValue) {
232                queryString = queryString + "&" + UifConstants.UrlParams.SHOW_HOME + "=false";
233            }
234    
235            queryString = queryString.replaceFirst("&", "");
236    
237            if (StringUtils.isNotEmpty(queryString)) {
238                url = url + "?" + queryString;
239            }
240    
241            this.setCurrent(form.getViewId(), form.getPageId(), buildViewTitle(form), url, form.getFormKey());
242        }
243    
244        /**
245         * Takes in the encoded history query parameter string passed on the url and parses it to create
246         * the list of historyEntries.  It will also append any homeward path if appendHomewardPath is true.  This
247         * append will happen after the passedHistory entries are appended so it will not make sense to use both settings
248         * in most cases.
249         *
250         * @param parameterString
251         */
252        public void buildHistoryFromParameterString(String parameterString) {
253            if (StringUtils.isNotEmpty(parameterString)) {
254                try {
255                    parameterString = URLDecoder.decode(parameterString, "UTF-8");
256                } catch (UnsupportedEncodingException e) {
257                    throw new RuntimeException("Unable to get history from parameter string", e);
258                }
259    
260                historyEntries = new ArrayList<HistoryEntry>();
261                if (appendPassedHistory) {
262                    String[] historyTokens = StringUtils.splitByWholeSeparator(parameterString, ENTRY_TOKEN);
263                    for (String token : historyTokens) {
264                        String[] params = StringUtils.splitByWholeSeparator(token, VAR_TOKEN);
265                        pushToHistory(params[0], params[1], params[2], params[3], params[4]);
266                    }
267                }
268            }
269    
270            if (appendHomewardPath) {
271                historyEntries.addAll(homewardPath);
272            }
273        }
274    
275        /**
276         * Gets the encoded and tokenized history parameter string that is representative of the HistoryEntries
277         * currently in History and includes the current view's HistoryEntry.  This parameter should be appended on any
278         * appropriate links which perform view swapping.
279         *
280         * @return String containing history entries
281         */
282        public String getHistoryParameterString() {
283            String history = "";
284    
285            for (HistoryEntry entry : historyEntries) {
286                if (StringUtils.isNotBlank(history)) {
287                    history = history + ENTRY_TOKEN;
288                }
289                history = history + entry.toParam();
290            }
291    
292            if (current != null) {
293                if (StringUtils.isNotBlank(history)) {
294                    history = history + ENTRY_TOKEN;
295                }
296                history = history + current.toParam();
297            }
298    
299            try {
300                history = URLEncoder.encode(history, "UTF-8");
301            } catch (Exception e) {
302                throw new RuntimeException("Error encoding history param", e);
303            }
304    
305            return history;
306        }
307    
308        /**
309         * Builds the title for the view to display in history (for example breadcrumbs)
310         *
311         * <p>
312         * Retrieves the viewLabelFieldPropertyName from the view if configured, otherwise attempts
313         * to find the title attribute for the default data object. If view label property is found the
314         * corresponding property value is retrieved and appended to the title for the view
315         * </p>
316         *
317         * TODO: Possibly move so it can be used for the actual view title, not just history
318         *
319         * @param form - form instance containing the view and view data
320         * @return String title string to use
321         */
322        protected String buildViewTitle(UifFormBase form) {
323            View view = form.getView();
324            String title = view.getHeaderText();
325    
326            // may move this into view logic instead in the future if it is required for the view's title (not just breadcrumb)
327            // if so remove this and just use getTitle - this logic would be in performFinalize instead
328            String viewLabelPropertyName = view.getBreadcrumbTitlePropertyName();
329    
330            // if view label property name given, try to retrieve the title attribute for the main data object
331            if (StringUtils.isBlank(viewLabelPropertyName)) {
332                Class<?> dataObjectClass;
333                if (StringUtils.isNotBlank(view.getDefaultBindingObjectPath())) {
334                    dataObjectClass = ObjectPropertyUtils.getPropertyType(form, view.getDefaultBindingObjectPath());
335                } else {
336                    dataObjectClass = view.getFormClass();
337                }
338    
339                DataObjectMetaDataService mds = KRADServiceLocatorWeb.getDataObjectMetaDataService();
340                if (dataObjectClass != null) {
341                    viewLabelPropertyName = mds.getTitleAttribute(dataObjectClass);
342                }
343            }
344    
345            String viewLabelPropertyPath = "";
346            if (StringUtils.isNotBlank(viewLabelPropertyName)) {
347                // adjust binding prefix
348                if (!viewLabelPropertyName.startsWith(UifConstants.NO_BIND_ADJUST_PREFIX)) {
349                    if (StringUtils.isNotBlank(view.getDefaultBindingObjectPath())) {
350                        viewLabelPropertyPath = view.getDefaultBindingObjectPath() + "." + viewLabelPropertyName;
351                    }
352                } else {
353                    viewLabelPropertyPath = StringUtils.removeStart(viewLabelPropertyName,
354                            UifConstants.NO_BIND_ADJUST_PREFIX);
355                }
356            } else {
357                // attempt to get title attribute
358                Class<?> dataObjectClass;
359                if (StringUtils.isNotBlank(view.getDefaultBindingObjectPath())) {
360                    dataObjectClass = ViewModelUtils.getObjectClassForMetadata(view, form,
361                            view.getDefaultBindingObjectPath());
362                } else {
363                    dataObjectClass = view.getFormClass();
364                }
365    
366                DataObjectMetaDataService mds = KRADServiceLocatorWeb.getDataObjectMetaDataService();
367                if (dataObjectClass != null) {
368                    String titleAttribute = mds.getTitleAttribute(dataObjectClass);
369                    if (StringUtils.isNotBlank(titleAttribute)) {
370                        viewLabelPropertyPath = view.getDefaultBindingObjectPath() + "." + titleAttribute;
371                    }
372                }
373            }
374    
375            Object viewLabelPropertyValue = null;
376            if (StringUtils.isNotBlank(viewLabelPropertyPath) && ObjectPropertyUtils.isReadableProperty(form,
377                    viewLabelPropertyPath)) {
378                viewLabelPropertyValue = ObjectPropertyUtils.getPropertyValue(form, viewLabelPropertyPath);
379            }
380    
381            String titleAppend = "";
382            if (viewLabelPropertyValue != null) {
383                titleAppend = viewLabelPropertyValue.toString();
384            }
385    
386            if (StringUtils.isNotBlank(titleAppend) && view.getBreadcrumbTitleDisplayOption() != null) {
387                if (view.getBreadcrumbTitleDisplayOption().equalsIgnoreCase(UifConstants.TitleAppendTypes.DASH)) {
388                    title = title + " - " + titleAppend;
389                } else if (view.getBreadcrumbTitleDisplayOption().equalsIgnoreCase(
390                        UifConstants.TitleAppendTypes.PARENTHESIS)) {
391                    title = title + "(" + titleAppend + ")";
392                } else if (view.getBreadcrumbTitleDisplayOption().equalsIgnoreCase(UifConstants.TitleAppendTypes.REPLACE)) {
393                    title = titleAppend;
394                }
395                //else it is none or blank so no title modification will be used
396            }
397    
398            return title;
399        }
400    
401        /**
402         * Gets the predetermined homeward path for this view's history.
403         * This is set by the same property in the view's Breadcrumbs configuration.
404         *
405         * @return the homewardPath
406         */
407        @BeanTagAttribute(name="homewardPath",type= BeanTagAttribute.AttributeType.LISTBEAN)
408        public List<HistoryEntry> getHomewardPath() {
409            return this.homewardPath;
410        }
411    
412        /**
413         * @param homewardPath the homewardPath to set
414         */
415        public void setHomewardPath(List<HistoryEntry> homewardPath) {
416            this.homewardPath = homewardPath;
417        }
418    
419        /**
420         * Gets a list of the current HistoryEntries not including the current entry.
421         * This list does not include the "&history=" query parameter on each HistoryEntry's
422         * url variable.  For HistoryEntries that include history information to be passed to the
423         * view they are retrieving, getGeneratedBreadcrumbs is used.
424         *
425         * @return the history
426         */
427        @BeanTagAttribute(name="historyEntries",type= BeanTagAttribute.AttributeType.LISTBEAN)
428        public List<HistoryEntry> getHistoryEntries() {
429            return this.historyEntries;
430        }
431    
432        /**
433         * @param history the history to set
434         */
435        public void setHistoryEntries(List<HistoryEntry> history) {
436            this.historyEntries = history;
437        }
438    
439        /**
440         * Gets the current view's HistoryEntry.
441         * This does not include the "&history=" query parameter on its
442         * url variable.  For the HistoryEntry that includes history information to be passed
443         * on the url it is retrieving, getGeneratedCurrentBreadcrumb is used.
444         *
445         * @return the current
446         */
447        @BeanTagAttribute(name="current",type= BeanTagAttribute.AttributeType.SINGLEBEAN)
448        public HistoryEntry getCurrent() {
449            return this.current;
450        }
451    
452        /**
453         * Sets the current HistoryEntry to the current view
454         *
455         * @param viewId
456         * @param pageId
457         * @param title
458         * @param url
459         * @param formKey
460         */
461        private void setCurrent(String viewId, String pageId, String title, String url, String formKey) {
462            HistoryEntry entry = new HistoryEntry(viewId, pageId, title, url, formKey);
463            current = entry;
464        }
465    
466        /**
467         * @param current the current to set
468         */
469        public void setCurrent(HistoryEntry current) {
470            this.current = current;
471        }
472    
473        /**
474         * When this is set to true, the homeward path will be appended.
475         * Note:  For most cases this should only be on during the first view load.
476         * This setting is set automatically in most cases.
477         *
478         * @param appendHomewardPath the appendHomewardPath to set
479         */
480        public void setAppendHomewardPath(boolean appendHomewardPath) {
481            this.appendHomewardPath = appendHomewardPath;
482        }
483    
484        /**
485         * @return the appendHomewardPath
486         */
487        @BeanTagAttribute(name="appendHomewardPath")
488        public boolean isAppendHomewardPath() {
489            return appendHomewardPath;
490        }
491    
492        /**
493         * Appends the passed history as each different view is shown.  This setting should be used when displaying
494         * passed history is relevant to the user (ie inquiry/lookup chains).  This setting is set automatically in
495         * most cases.
496         *
497         * @param appendPassedHistory the appendPassedHistory to set
498         */
499        public void setAppendPassedHistory(boolean appendPassedHistory) {
500            this.appendPassedHistory = appendPassedHistory;
501        }
502    
503        /**
504         * @return the appendPassedHistory
505         */
506        @BeanTagAttribute(name="appendPassedHistory")
507        public boolean isAppendPassedHistory() {
508            return appendPassedHistory;
509        }
510    
511    }