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.service.DataObjectMetaDataService; 021 import org.kuali.rice.krad.service.KRADServiceLocatorWeb; 022 import org.kuali.rice.krad.uif.UifConstants; 023 import org.kuali.rice.krad.uif.util.ObjectPropertyUtils; 024 import org.kuali.rice.krad.uif.util.ViewModelUtils; 025 import org.kuali.rice.krad.web.form.UifFormBase; 026 027 import javax.servlet.http.HttpServletRequest; 028 import java.io.Serializable; 029 import java.io.UnsupportedEncodingException; 030 import java.net.URLDecoder; 031 import java.net.URLEncoder; 032 import java.util.ArrayList; 033 import java.util.Enumeration; 034 import 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 */ 042 public 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 }