001/**
002 * Copyright 2005-2016 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.view;
017
018import org.apache.commons.lang.StringUtils;
019import org.apache.log4j.Logger;
020import org.kuali.rice.krad.service.DataObjectMetaDataService;
021import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
022import org.kuali.rice.krad.uif.UifConstants;
023import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
024import org.kuali.rice.krad.uif.util.ViewModelUtils;
025import org.kuali.rice.krad.web.form.UifFormBase;
026
027import javax.servlet.http.HttpServletRequest;
028import java.io.Serializable;
029import java.io.UnsupportedEncodingException;
030import java.net.URLDecoder;
031import java.net.URLEncoder;
032import java.util.ArrayList;
033import java.util.Enumeration;
034import 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 */
042public 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}