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 }