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