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 }