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 }