View Javadoc

1   /**
2    * Copyright 2005-2011 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.uif.view;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.apache.log4j.Logger;
20  import org.kuali.rice.krad.service.DataObjectMetaDataService;
21  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
22  import org.kuali.rice.krad.uif.UifConstants;
23  import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
24  import org.kuali.rice.krad.uif.util.ViewModelUtils;
25  import org.kuali.rice.krad.web.form.UifFormBase;
26  
27  import javax.servlet.http.HttpServletRequest;
28  import java.io.Serializable;
29  import java.io.UnsupportedEncodingException;
30  import java.net.URLDecoder;
31  import java.net.URLEncoder;
32  import java.util.ArrayList;
33  import java.util.Enumeration;
34  import java.util.List;
35  
36  /**
37   * History class used to keep track of views visited so they can be displayed in the ui
38   * as breadcrumbs - both as homeward path and history path interpretations
39   *
40   * @author Kuali Rice Team (rice.collab@kuali.org)
41   */
42  public class History implements Serializable {
43      private static final long serialVersionUID = -8279297694371557335L;
44      private static final Logger LOG = Logger.getLogger(History.class);
45  
46      public static final String ENTRY_TOKEN = "$";
47      public static final String VAR_TOKEN = ",";
48  
49      private boolean appendHomewardPath;
50      private boolean appendPassedHistory;
51  
52      private HistoryEntry current;
53  
54      private List<HistoryEntry> homewardPath;
55      private List<HistoryEntry> historyEntries;
56  
57      public History() {
58          historyEntries = new ArrayList<HistoryEntry>();
59      }
60  
61      /**
62       * Gets the predetermined homeward path for this view's history.
63       * This is set by the same property in the view's Breadcrumbs configuration.
64       *
65       * @return the homewardPath
66       */
67      public List<HistoryEntry> getHomewardPath() {
68          return this.homewardPath;
69      }
70  
71      /**
72       * @param homewardPath the homewardPath to set
73       */
74      public void setHomewardPath(List<HistoryEntry> homewardPath) {
75          this.homewardPath = homewardPath;
76      }
77  
78      /**
79       * Gets a list of the current HistoryEntries not including the current entry.
80       * This list does not include the "&history=" query parameter on each HistoryEntry's
81       * url variable.  For HistoryEntries that include history information to be passed to the
82       * view they are retrieving, getGeneratedBreadcrumbs is used.
83       *
84       * @return the history
85       */
86      public List<HistoryEntry> getHistoryEntries() {
87          return this.historyEntries;
88      }
89  
90      /**
91       * @param history the history to set
92       */
93      public void setHistoryEntries(List<HistoryEntry> history) {
94          this.historyEntries = history;
95      }
96  
97      /**
98       * Gets the current view's HistoryEntry.
99       * 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      * @param current the current to set
111      */
112     private void setCurrent(String viewId, String pageId, String title, String url, String formKey) {
113         HistoryEntry entry = new HistoryEntry(viewId, pageId, title, url, formKey);
114         current = entry;
115     }
116 
117     /**
118      * @param current the current to set
119      */
120     public void setCurrent(HistoryEntry current) {
121         this.current = current;
122     }
123 
124     /**
125      * Takes in the encoded history query parameter string passed on the url and parses it to create
126      * the list of historyEntries.  It will also append any homeward path if appendHomewardPath is true.  This
127      * append will happen after the passedHistory entries are appended so it will not make sense to use both settings
128      * in most cases.
129      *
130      * @param parameterString
131      */
132     public void buildHistoryFromParameterString(String parameterString) {
133         if (StringUtils.isNotEmpty(parameterString)) {
134             try {
135                 parameterString = URLDecoder.decode(parameterString, "UTF-8");
136             } catch (UnsupportedEncodingException e) {
137                 LOG.error("Error decoding history param", e);
138             }
139 
140             historyEntries = new ArrayList<HistoryEntry>();
141             if (appendPassedHistory) {
142                 String[] historyTokens = parameterString.split("\\" + ENTRY_TOKEN);
143                 for (String token : historyTokens) {
144                     String[] params = token.split(VAR_TOKEN);
145                     pushToHistory(params[0], params[1], params[2], params[3], params[4]);
146                 }
147             }
148         }
149 
150         if (appendHomewardPath) {
151             historyEntries.addAll(homewardPath);
152         }
153     }
154 
155     /**
156      * Gets the encoded and tokenized history parameter string that is representative of the HistoryEntries
157      * currently in History and includes the current view's HistoryEntry.  This parameter should be appended on any
158      * appropriate links which perform view swapping.
159      *
160      * @return
161      */
162     public String getHistoryParameterString() {
163         String historyString = "";
164         for (HistoryEntry e : historyEntries) {
165             if (historyEntries.indexOf(e) == 0) {
166                 historyString = historyString + e.toParam();
167             } else {
168                 historyString = historyString + ENTRY_TOKEN + e.toParam();
169             }
170         }
171 
172         // add current
173         if (current != null) {
174             if (historyString.equals("")) {
175                 historyString = historyString + current.toParam();
176             } else {
177                 historyString = historyString + ENTRY_TOKEN + current.toParam();
178             }
179         }
180 
181         try {
182             historyString = URLEncoder.encode(historyString, "UTF-8");
183         } catch (Exception e) {
184             LOG.error("Error encoding history param", e);
185         }
186 
187         return historyString;
188     }
189 
190     /**
191      * Generates a list of HistoryEntries that can be used as breadcrumbs by the breadcrumb widget.  This
192      * method appends the appropriate history information on the HistoryEntry url variables so when a view is requested
193      * its history can be regenerated for use in its breadcrumbs.  It also sets the the passed showHome variable to
194      * false to prevent showing the homeward path more than once (as it is passed through the history
195      * variable backwards). This does not include the current HistoryEntry as a breadcrumb.
196      *
197      * @return
198      */
199     public List<HistoryEntry> getGeneratedBreadcrumbs() {
200         List<HistoryEntry> breadcrumbs = new ArrayList<HistoryEntry>();
201         for (int i = 0; i < historyEntries.size(); i++) {
202             if (i == 0) {
203                 breadcrumbs.add(copyEntry(historyEntries.get(i)));
204             } else {
205                 HistoryEntry breadcrumb = copyEntry(historyEntries.get(i));
206                 String historyParam = "";
207                 for (int j = 0; j < i; j++) {
208                     historyParam = historyParam + ENTRY_TOKEN + historyEntries.get(j).toParam();
209                 }
210                 historyParam = historyParam.replaceFirst("\\" + ENTRY_TOKEN, "");
211                 try {
212                     historyParam = URLEncoder.encode(historyParam, "UTF-8");
213                 } catch (Exception e) {
214                     LOG.error("Error encoding history param", e);
215                 }
216 
217                 String url = "";
218                 if (breadcrumb.getUrl().contains("?")) {
219                     url = breadcrumb.getUrl() + "&" + UifConstants.UrlParams.HISTORY + "=" + historyParam;
220                 } else {
221                     url = breadcrumb.getUrl() + "?" + UifConstants.UrlParams.HISTORY + "=" + historyParam;
222                 }
223 
224                 breadcrumb.setUrl(url);
225                 breadcrumbs.add(breadcrumb);
226             }
227         }
228 
229         return breadcrumbs;
230     }
231 
232     /**
233      * Gets the current HistoryEntry in the breadcrumb format described in getGeneratedBreadcrumbs
234      *
235      * @return
236      */
237     public HistoryEntry getGeneratedCurrentBreadcrumb() {
238         HistoryEntry breadcrumb = copyEntry(current);
239         String historyParam = "";
240         for (int j = 0; j < historyEntries.size(); j++) {
241             historyParam = historyParam + ENTRY_TOKEN + historyEntries.get(j).toParam();
242         }
243         historyParam = historyParam.replaceFirst("\\" + ENTRY_TOKEN, "");
244 
245         try {
246             historyParam = URLEncoder.encode(historyParam, "UTF-8");
247         } catch (Exception e) {
248             LOG.error("Error encoding history param", e);
249         }
250 
251         String url = "";
252         if (breadcrumb.getUrl().contains("?")) {
253             url = breadcrumb.getUrl() + "&" + UifConstants.UrlParams.HISTORY + "=" + historyParam;
254         } else {
255             url = breadcrumb.getUrl() + "?" + UifConstants.UrlParams.HISTORY + "=" + historyParam;
256         }
257         breadcrumb.setUrl(url);
258 
259         return breadcrumb;
260     }
261 
262     /**
263      * Copies a HistoryEntry, for use during breadcrumb generation.
264      *
265      * @param e
266      * @return
267      */
268     private HistoryEntry copyEntry(HistoryEntry e) {
269         return new HistoryEntry(e.getViewId(), e.getPageId(), e.getTitle(), e.getUrl(), e.getFormKey());
270     }
271 
272     /**
273      * Pushes the information passed in to history.
274      * Note: currently only used internally in the class - be cautious about its external use.
275      *
276      * @param viewId
277      * @param pageId
278      * @param title
279      * @param url
280      * @param formKey
281      */
282     public void pushToHistory(String viewId, String pageId, String title, String url, String formKey) {
283         HistoryEntry entry = new HistoryEntry(viewId, pageId, title, url, formKey);
284         historyEntries.add(entry);
285     }
286 
287     /**
288      * When this is set to true, the homeward path will be appended.
289      * Note:  For most cases this should only be on during the first view load.
290      * This setting is set automatically in most cases.
291      *
292      * @param appendHomewardPath the appendHomewardPath to set
293      */
294     public void setAppendHomewardPath(boolean appendHomewardPath) {
295         this.appendHomewardPath = appendHomewardPath;
296     }
297 
298     /**
299      * @return the appendHomewardPath
300      */
301     public boolean isAppendHomewardPath() {
302         return appendHomewardPath;
303     }
304 
305     /**
306      * Appends the passed history as each different view is shown.  This setting should be used when displaying
307      * passed history is relevant to the user (ie inquiry/lookup chains).  This setting is set automatically in
308      * most cases.
309      *
310      * @param appendPassedHistory the appendPassedHistory to set
311      */
312     public void setAppendPassedHistory(boolean appendPassedHistory) {
313         this.appendPassedHistory = appendPassedHistory;
314     }
315 
316     /**
317      * @return the appendPassedHistory
318      */
319     public boolean isAppendPassedHistory() {
320         return appendPassedHistory;
321     }
322 
323     /**
324      * Sets the current HistoryEntry using information from the form and the request.  This history parameter is
325      * extracted out of the url inorder for a "clean" url to be used in history parameter and
326      * breadcrumb generation, as passing history history through the nested urls is unnecessary.
327      *
328      * @param form
329      * @param request
330      */
331     @SuppressWarnings("unchecked")
332     public void setCurrent(UifFormBase form, HttpServletRequest request) {
333         if (!request.getMethod().equals("POST")) {
334             boolean showHomeValue = false;
335             boolean pageIdValue = false;
336             boolean formKeyValue = false;
337 
338             String queryString = "";
339             String url = request.getRequestURL().toString();
340 
341             //remove history attribute
342             Enumeration<String> params = request.getParameterNames();
343             while (params.hasMoreElements()) {
344                 String key = params.nextElement();
345                 if (!key.equals(UifConstants.UrlParams.HISTORY)) {
346                     for (String value : request.getParameterValues(key)) {
347                         queryString = queryString + "&" + key + "=" + value;
348                     }
349                 } else if (key.equals(UifConstants.UrlParams.PAGE_ID)) {
350                     pageIdValue = true;
351                 } else if (key.equals(UifConstants.UrlParams.SHOW_HOME)) {
352                     showHomeValue = true;
353                 } else if (key.equals(UifConstants.UrlParams.FORM_KEY)) {
354                     formKeyValue = true;
355                 }
356             }
357 
358             //add formKey and pageId to url
359             if (StringUtils.isNotBlank(form.getFormKey()) && !formKeyValue) {
360                 queryString = queryString + "&" + UifConstants.UrlParams.FORM_KEY + "=" + form.getFormKey();
361             }
362             if (StringUtils.isNotBlank(form.getPageId()) && !pageIdValue) {
363                 queryString = queryString + "&" + UifConstants.UrlParams.PAGE_ID + "=" + form.getPageId();
364             }
365             if (!showHomeValue) {
366                 queryString = queryString + "&" + UifConstants.UrlParams.SHOW_HOME + "=false";
367             }
368 
369             queryString = queryString.replaceFirst("&", "");
370 
371             if (StringUtils.isNotEmpty(queryString)) {
372                 url = url + "?" + queryString;
373             }
374 
375             this.setCurrent(form.getViewId(), form.getPageId(), buildViewTitle(form), url, form.getFormKey());
376         }
377     }
378 
379     /**
380      * Builds the title for the view to display in history (for example breadcrumbs)
381      *
382      * <p>
383      * Retrieves the viewLabelFieldPropertyName from the view if configured, otherwise attempts
384      * to find the title attribute for the default data object. If view label property is found the
385      * corresponding property value is retrieved and appended to the title for the view
386      * </p>
387      *
388      * TODO: Possibly move so it can be used for the actual view title, not just history
389      *
390      * @param form - form instance containing the view and view data
391      * @return String title string to use
392      */
393     protected String buildViewTitle(UifFormBase form) {
394         View view = form.getView();
395         String title = view.getTitle();
396 
397         // may move this into view logic instead in the future if it is required for the view's title (not just breadcrumb)
398         // if so remove this and just use getTitle - this logic would be in performFinalize instead
399         String viewLabelPropertyName = view.getViewLabelFieldPropertyName();
400 
401         // if view label property name given, try to retrieve the title attribute for the main data object
402         if (StringUtils.isBlank(viewLabelPropertyName)) {
403             Class<?> dataObjectClass;
404             if (StringUtils.isNotBlank(view.getDefaultBindingObjectPath())) {
405                 dataObjectClass = ObjectPropertyUtils.getPropertyType(form, view.getDefaultBindingObjectPath());
406             } else {
407                 dataObjectClass = view.getFormClass();
408             }
409 
410             DataObjectMetaDataService mds = KRADServiceLocatorWeb.getDataObjectMetaDataService();
411             if (dataObjectClass != null) {
412                 viewLabelPropertyName = mds.getTitleAttribute(dataObjectClass);
413             }
414         }
415 
416         String viewLabelPropertyPath = "";
417         if (StringUtils.isNotBlank(viewLabelPropertyName)) {
418             // adjust binding prefix
419             if (!viewLabelPropertyName.startsWith(UifConstants.NO_BIND_ADJUST_PREFIX)) {
420                 if (StringUtils.isNotBlank(view.getDefaultBindingObjectPath())) {
421                     viewLabelPropertyPath = view.getDefaultBindingObjectPath() + "." + viewLabelPropertyName;
422                 }
423             } else {
424                 viewLabelPropertyPath = StringUtils.removeStart(viewLabelPropertyName,
425                         UifConstants.NO_BIND_ADJUST_PREFIX);
426             }
427         }
428         else {
429             // attempt to get title attribute
430             Class<?> dataObjectClass;
431             if (StringUtils.isNotBlank(view.getDefaultBindingObjectPath())) {
432                 dataObjectClass = ViewModelUtils.getObjectClassForMetadata(view, form,
433                         view.getDefaultBindingObjectPath());
434             } else {
435                 dataObjectClass = view.getFormClass();
436             }
437 
438             DataObjectMetaDataService mds = KRADServiceLocatorWeb.getDataObjectMetaDataService();
439             if (dataObjectClass != null) {
440                 String titleAttribute = mds.getTitleAttribute(dataObjectClass);
441                 if (StringUtils.isNotBlank(titleAttribute)) {
442                   viewLabelPropertyPath = view.getDefaultBindingObjectPath() + "." + titleAttribute;
443                 }
444             }
445         }
446 
447         Object viewLabelPropertyValue = null;
448         if (StringUtils.isNotBlank(viewLabelPropertyPath)) {
449             viewLabelPropertyValue = ObjectPropertyUtils.getPropertyValue(form, viewLabelPropertyPath);
450         }
451 
452         String titleAppend = "";
453         if (viewLabelPropertyValue != null) {
454             titleAppend = viewLabelPropertyValue.toString();
455         }
456 
457         if (StringUtils.isNotBlank(titleAppend) && view.getAppendOption() != null) {
458             if (view.getAppendOption().equalsIgnoreCase(UifConstants.TitleAppendTypes.DASH)) {
459                 title = title + " - " + titleAppend;
460             } else if (view.getAppendOption().equalsIgnoreCase(UifConstants.TitleAppendTypes.PARENTHESIS)) {
461                 title = title + "(" + titleAppend + ")";
462             } else if (view.getAppendOption().equalsIgnoreCase(UifConstants.TitleAppendTypes.REPLACE)) {
463                 title = titleAppend;
464             }
465             //else it is none or blank so no title modification will be used
466         }
467 
468         return title;
469     }
470 }