View Javadoc

1   /**
2    * Copyright 2005-2012 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.datadictionary.parse.BeanTag;
21  import org.kuali.rice.krad.datadictionary.parse.BeanTagAttribute;
22  import org.kuali.rice.krad.service.DataObjectMetaDataService;
23  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
24  import org.kuali.rice.krad.uif.UifConstants;
25  import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
26  import org.kuali.rice.krad.uif.util.ViewModelUtils;
27  import org.kuali.rice.krad.web.form.UifFormBase;
28  
29  import javax.servlet.http.HttpServletRequest;
30  import java.io.Serializable;
31  import java.io.UnsupportedEncodingException;
32  import java.net.URLDecoder;
33  import java.net.URLEncoder;
34  import java.util.ArrayList;
35  import java.util.Enumeration;
36  import java.util.List;
37  
38  /**
39   * History class used to keep track of views visited so they can be displayed in the ui
40   * as breadcrumbs - both as homeward path and history path interpretations
41   *
42   * @author Kuali Rice Team (rice.collab@kuali.org)
43   */
44  @BeanTag(name="history")
45  public class History implements Serializable {
46      private static final long serialVersionUID = -8279297694371557335L;
47      private static final Logger LOG = Logger.getLogger(History.class);
48  
49      public static final String ENTRY_TOKEN = "$^$";
50      public static final String VAR_TOKEN = "^$^";
51  
52      private boolean appendHomewardPath;
53      private boolean appendPassedHistory;
54  
55      private HistoryEntry current;
56  
57      private List<HistoryEntry> homewardPath;
58      private List<HistoryEntry> historyEntries;
59  
60      public History() {
61          historyEntries = new ArrayList<HistoryEntry>();
62      }
63  
64      /**
65       * Generates a list of HistoryEntries that can be used as breadcrumbs by the breadcrumb widget.  This
66       * method appends the appropriate history information on the HistoryEntry url variables so when a view is requested
67       * its history can be regenerated for use in its breadcrumbs.  It also sets the the passed showHome variable to
68       * false to prevent showing the homeward path more than once (as it is passed through the history
69       * variable backwards). This does not include the current HistoryEntry as a breadcrumb but adds the formKey as the
70       * LAST_FORM_KEY to assist with server side form cleanup.
71       *
72       * @return
73       */
74      public List<HistoryEntry> getGeneratedBreadcrumbs() {
75          List<HistoryEntry> breadcrumbs = new ArrayList<HistoryEntry>();
76          for (int i = 0; i < historyEntries.size(); i++) {
77              if (i == 0) {
78                  breadcrumbs.add(copyEntry(historyEntries.get(i)));
79              } else {
80                  HistoryEntry breadcrumb = copyEntry(historyEntries.get(i));
81  
82                  String historyParam = "";
83                  for (int j = 0; j < i; j++) {
84                      historyParam = historyParam + ENTRY_TOKEN + historyEntries.get(j).toParam();
85                  }
86                  historyParam = historyParam.replaceFirst("\\" + ENTRY_TOKEN, "");
87  
88                  try {
89                      historyParam = URLEncoder.encode(historyParam, "UTF-8");
90                  } catch (Exception e) {
91                      throw new RuntimeException("Error encoding history param", e);
92                  }
93  
94                  if (StringUtils.isEmpty(breadcrumb.getUrl())) {
95                      continue;
96                  }
97  
98                  String url = breadcrumb.getUrl();
99                  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 }