View Javadoc

1   /**
2    * Copyright 2005-2014 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.kns.util;
17  
18  import java.io.ByteArrayOutputStream;
19  import java.io.IOException;
20  import java.io.InputStream;
21  import java.io.OutputStream;
22  import java.util.Arrays;
23  import java.util.Collection;
24  import java.util.Enumeration;
25  import java.util.HashMap;
26  import java.util.Hashtable;
27  import java.util.Iterator;
28  import java.util.LinkedHashMap;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.Set;
32  import java.util.regex.Matcher;
33  import java.util.regex.Pattern;
34  
35  import javax.servlet.ServletException;
36  import javax.servlet.http.HttpServletRequest;
37  import javax.servlet.http.HttpServletResponse;
38  import javax.servlet.http.HttpSession;
39  import javax.servlet.jsp.PageContext;
40  
41  import org.apache.commons.lang.StringEscapeUtils;
42  import org.apache.commons.lang.StringUtils;
43  import org.apache.log4j.Level;
44  import org.apache.log4j.Logger;
45  import org.apache.struts.Globals;
46  import org.apache.struts.action.ActionForm;
47  import org.apache.struts.action.ActionMapping;
48  import org.apache.struts.action.ActionServletWrapper;
49  import org.apache.struts.upload.CommonsMultipartRequestHandler;
50  import org.apache.struts.upload.FormFile;
51  import org.apache.struts.upload.MultipartRequestHandler;
52  import org.apache.struts.upload.MultipartRequestWrapper;
53  import org.kuali.rice.core.api.config.property.ConfigurationService;
54  import org.kuali.rice.core.api.util.RiceKeyConstants;
55  import org.kuali.rice.kew.api.action.ActionRequest;
56  import org.kuali.rice.kew.api.action.RecipientType;
57  import org.kuali.rice.kim.api.role.Role;
58  import org.kuali.rice.kim.api.services.KimApiServiceLocator;
59  import org.kuali.rice.kns.datadictionary.KNSDocumentEntry;
60  import org.kuali.rice.kns.datadictionary.MaintenanceDocumentEntry;
61  import org.kuali.rice.kns.document.authorization.DocumentAuthorizer;
62  import org.kuali.rice.kns.service.KNSServiceLocator;
63  import org.kuali.rice.kns.web.struts.action.KualiAction;
64  import org.kuali.rice.kns.web.struts.action.KualiMultipartRequestHandler;
65  import org.kuali.rice.kns.web.struts.form.InquiryForm;
66  import org.kuali.rice.kns.web.struts.form.KualiDocumentFormBase;
67  import org.kuali.rice.kns.web.struts.form.KualiForm;
68  import org.kuali.rice.kns.web.struts.form.KualiMaintenanceForm;
69  import org.kuali.rice.kns.web.struts.form.pojo.PojoFormBase;
70  import org.kuali.rice.kns.web.ui.Field;
71  import org.kuali.rice.kns.web.ui.Row;
72  import org.kuali.rice.kns.web.ui.Section;
73  import org.kuali.rice.krad.datadictionary.AttributeDefinition;
74  import org.kuali.rice.krad.datadictionary.AttributeSecurity;
75  import org.kuali.rice.krad.datadictionary.DataDictionary;
76  import org.kuali.rice.krad.datadictionary.DataDictionaryEntryBase;
77  import org.kuali.rice.krad.datadictionary.mask.MaskFormatter;
78  import org.kuali.rice.krad.document.Document;
79  import org.kuali.rice.krad.exception.ValidationException;
80  import org.kuali.rice.krad.service.KRADServiceLocator;
81  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
82  import org.kuali.rice.krad.util.GlobalVariables;
83  import org.kuali.rice.krad.util.KRADConstants;
84  import org.kuali.rice.krad.util.MessageMap;
85  import org.kuali.rice.krad.util.ObjectUtils;
86  
87  /**
88   * General helper methods for handling requests.
89   */
90  public class WebUtils {
91  	private static final Logger LOG = Logger.getLogger(WebUtils.class);
92  
93  	private static final String IMAGE_COORDINATE_CLICKED_X_EXTENSION = ".x";
94  	private static final String IMAGE_COORDINATE_CLICKED_Y_EXTENSION = ".y";
95  
96  	private static final String APPLICATION_IMAGE_URL_PROPERTY_PREFIX = "application.custom.image.url";
97  	private static final String DEFAULT_IMAGE_URL_PROPERTY_NAME = "kr.externalizable.images.url";
98  
99      /**
100      * Prefixes indicating an absolute url
101      */
102     private static final String[] SCHEMES = { "http://", "https://" };
103 
104 	/**
105 	 * A request attribute name that indicates that a
106 	 * {@link org.kuali.rice.kns.exception.FileUploadLimitExceededException} has already been thrown for the
107 	 * request.
108 	 */
109 	public static final String FILE_UPLOAD_LIMIT_EXCEEDED_EXCEPTION_ALREADY_THROWN = "fileUploadLimitExceededExceptionAlreadyThrown";
110 
111 	private static ConfigurationService configurationService;
112 
113 	/**
114 	 * Checks for methodToCall parameter, and picks off the value using set dot
115 	 * notation. Handles the problem of image submits.
116 	 * 
117 	 * @param request
118 	 * @return methodToCall String
119 	 */
120 	public static String parseMethodToCall(ActionForm form, HttpServletRequest request) {
121 		String methodToCall = null;
122 
123 		// check if is specified cleanly
124 		if (StringUtils.isNotBlank(request.getParameter(KRADConstants.DISPATCH_REQUEST_PARAMETER))) {
125 			if (form instanceof KualiForm
126 					&& !((KualiForm) form).shouldMethodToCallParameterBeUsed(KRADConstants.DISPATCH_REQUEST_PARAMETER,
127 							request.getParameter(KRADConstants.DISPATCH_REQUEST_PARAMETER), request)) {
128 				throw new RuntimeException("Cannot verify that the methodToCall should be "
129 						+ request.getParameter(KRADConstants.DISPATCH_REQUEST_PARAMETER));
130 			}
131 			methodToCall = request.getParameter(KRADConstants.DISPATCH_REQUEST_PARAMETER);
132 			// include .x at the end of the parameter to make it consistent w/
133 			// other parameters
134 			request.setAttribute(KRADConstants.METHOD_TO_CALL_ATTRIBUTE, KRADConstants.DISPATCH_REQUEST_PARAMETER + "."
135 					+ methodToCall + IMAGE_COORDINATE_CLICKED_X_EXTENSION);
136 		}
137 
138 		/**
139 		 * The reason why we are checking for a ".x" at the end of the parameter
140 		 * name: It is for the image names that in addition to sending the form
141 		 * data, the web browser sends the x,y coordinate of where the user
142 		 * clicked on the image. If the image input is not given a name then the
143 		 * browser sends the x and y coordinates as the "x" and "y" input
144 		 * fields. If the input image does have a name, the x and y coordinates
145 		 * are sent using the format name.x and name.y.
146 		 */
147 		if (methodToCall == null) {
148 			// iterate through parameters looking for methodToCall
149 			for (Enumeration i = request.getParameterNames(); i.hasMoreElements();) {
150 				String parameterName = (String) i.nextElement();
151 
152 				// check if the parameter name is a specifying the methodToCall
153 				if (isMethodToCall(parameterName)) {
154 					methodToCall = getMethodToCallSettingAttribute(form, request, parameterName);
155 					break;
156 				}
157 				else {
158 					// KULRICE-1218: Check if the parameter's values match (not
159 					// just the name)
160 					for (String value : request.getParameterValues(parameterName)) {
161 						// adding period to startsWith check - don't want to get
162 						// confused with methodToCallFoobar
163 						if (isMethodToCall(value)) {
164 							methodToCall = getMethodToCallSettingAttribute(form, request, value);
165 							// why is there not a break outer loop here?
166 						}
167 					}
168 				}
169 			}
170 		}
171 
172 		return methodToCall;
173 	}
174 
175     /**
176 	 * Checks if a string signifies a methodToCall string
177 	 * 
178 	 * @param string
179 	 *            the string to check
180 	 * @return true if is a methodToCall
181 	 */
182 	private static boolean isMethodToCall(String string) {
183 		// adding period to startsWith check - don't want to get confused with
184 		// methodToCallFoobar
185 		return string.startsWith(KRADConstants.DISPATCH_REQUEST_PARAMETER + ".");
186 	}
187 
188 	/**
189 	 * Parses out the methodToCall command and also sets the request attribute
190 	 * for the methodToCall.
191 	 * 
192 	 * @param form
193 	 *            the ActionForm
194 	 * @param request
195 	 *            the request to set the attribute on
196 	 * @param string
197 	 *            the methodToCall string
198 	 * @return the methodToCall command
199 	 */
200 	private static String getMethodToCallSettingAttribute(ActionForm form, HttpServletRequest request, String string) {
201 
202 		if (form instanceof KualiForm
203 				&& !((KualiForm) form).shouldMethodToCallParameterBeUsed(string, request.getParameter(string), request)) {
204 			throw new RuntimeException("Cannot verify that the methodToCall should be " + string);
205 		}
206 		// always adding a coordinate even if not an image
207 		final String attributeValue = endsWithCoordinates(string) ? string : string
208 				+ IMAGE_COORDINATE_CLICKED_X_EXTENSION;
209 		final String methodToCall = StringUtils.substringBetween(attributeValue,
210 				KRADConstants.DISPATCH_REQUEST_PARAMETER + ".", ".");
211 		request.setAttribute(KRADConstants.METHOD_TO_CALL_ATTRIBUTE, attributeValue);
212 		return methodToCall;
213 	}
214 
215 	/**
216 	 * Iterates through and logs (at the given level) all attributes and
217 	 * parameters of the given request onto the given Logger
218 	 * 
219 	 * @param request
220 	 * @param logger
221 	 */
222 	public static void logRequestContents(Logger logger, Level level, HttpServletRequest request) {
223 		if (logger.isEnabledFor(level)) {
224 			logger.log(level, "--------------------");
225 			logger.log(level, "HttpRequest attributes:");
226 			for (Enumeration e = request.getAttributeNames(); e.hasMoreElements();) {
227 				String attrName = (String) e.nextElement();
228 				Object attrValue = request.getAttribute(attrName);
229 
230 				if (attrValue.getClass().isArray()) {
231 					logCollection(logger, level, attrName, Arrays.asList((Object[]) attrValue));
232 				}
233 				else if (attrValue instanceof Collection) {
234 					logCollection(logger, level, attrName, (Collection) attrValue);
235 				}
236 				else if (attrValue instanceof Map) {
237 					logMap(logger, level, attrName, (Map) attrValue);
238 				}
239 				else {
240 					logObject(logger, level, attrName, attrValue);
241 				}
242 			}
243 
244 			logger.log(level, "--------------------");
245 			logger.log(level, "HttpRequest parameters:");
246 			for (Enumeration i = request.getParameterNames(); i.hasMoreElements();) {
247 				String paramName = (String) i.nextElement();
248 				String[] paramValues = (String[]) request.getParameterValues(paramName);
249 
250 				logArray(logger, level, paramName, paramValues);
251 			}
252 
253 			logger.log(level, "--------------------");
254 		}
255 	}
256 
257 	private static void logArray(Logger logger, Level level, String arrayName, Object[] array) {
258 		StringBuffer value = new StringBuffer("[");
259 		for (int i = 0; i < array.length; ++i) {
260 			if (i > 0) {
261 				value.append(",");
262 			}
263 			value.append(array[i]);
264 		}
265 		value.append("]");
266 
267 		logThing(logger, level, arrayName, value);
268 	}
269 
270 	private static void logCollection(Logger logger, Level level, String collectionName, Collection c) {
271 		StringBuffer value = new StringBuffer("{");
272 		for (Iterator i = c.iterator(); i.hasNext();) {
273 			value.append(i.next());
274 			if (i.hasNext()) {
275 				value.append(",");
276 			}
277 		}
278 		value.append("}");
279 
280 		logThing(logger, level, collectionName, value);
281 	}
282 
283 	private static void logMap(Logger logger, Level level, String mapName, Map m) {
284 		StringBuffer value = new StringBuffer("{");
285 		for (Iterator i = m.entrySet().iterator(); i.hasNext();) {
286 			Map.Entry e = (Map.Entry) i.next();
287 			value.append("('" + e.getKey() + "','" + e.getValue() + "')");
288 		}
289 		value.append("}");
290 
291 		logThing(logger, level, mapName, value);
292 	}
293 
294 	private static void logObject(Logger logger, Level level, String objectName, Object o) {
295 		logThing(logger, level, objectName, "'" + o + "'");
296 	}
297 
298 	private static void logThing(Logger logger, Level level, String thingName, Object thing) {
299 		logger.log(level, "    '" + thingName + "' => " + thing);
300 	}
301 
302 	/**
303 	 * A file that is not of type text/plain or text/html can be output through
304 	 * the response using this method.
305 	 * 
306 	 * @param response
307 	 * @param contentType
308 	 * @param byteArrayOutputStream
309 	 * @param fileName
310 	 */
311 	public static void saveMimeOutputStreamAsFile(HttpServletResponse response, String contentType,
312 			ByteArrayOutputStream byteArrayOutputStream, String fileName) throws IOException {
313 
314         // If there are quotes in the name, we should replace them to avoid issues.
315         // The filename will be wrapped with quotes below when it is set in the header
316         String updateFileName;
317         if(fileName.contains("\"")) {
318             updateFileName = fileName.replaceAll("\"", "");
319         } else {
320             updateFileName =  fileName;
321         }
322 
323 		// set response
324 		response.setContentType(contentType);
325         response.setHeader("Content-disposition", "attachment; filename=\"" + updateFileName + "\"");
326         response.setHeader("Expires", "0");
327 		response.setHeader("Cache-Control", "must-revalidate, post-check=0, pre-check=0");
328 		response.setHeader("Pragma", "public");
329 		response.setContentLength(byteArrayOutputStream.size());
330 
331 		// write to output
332 		OutputStream outputStream = response.getOutputStream();
333 		byteArrayOutputStream.writeTo(response.getOutputStream());
334 		outputStream.flush();
335 		outputStream.close();
336 	}
337 
338 	/**
339 	 * A file that is not of type text/plain or text/html can be output through
340 	 * the response using this method.
341 	 * 
342 	 * @param response
343 	 * @param contentType
344 	 * @param inStream
345 	 * @param fileName
346 	 */
347 	public static void saveMimeInputStreamAsFile(HttpServletResponse response, String contentType,
348 			InputStream inStream, String fileName, int fileSize) throws IOException {
349 
350         // If there are quotes in the name, we should replace them to avoid issues.
351         // The filename will be wrapped with quotes below when it is set in the header
352         String updateFileName;
353         if(fileName.contains("\"")) {
354             updateFileName = fileName.replaceAll("\"", "");
355         } else {
356             updateFileName =  fileName;
357         }
358 
359 		// set response
360 		response.setContentType(contentType);
361         response.setHeader("Content-disposition", "attachment; filename=\"" + updateFileName + "\"");
362         response.setHeader("Expires", "0");
363 		response.setHeader("Cache-Control", "must-revalidate, post-check=0, pre-check=0");
364 		response.setHeader("Pragma", "public");
365 		response.setContentLength(fileSize);
366 
367 		// write to output
368 		OutputStream out = response.getOutputStream();
369 		while (inStream.available() > 0) {
370 			out.write(inStream.read());
371 		}
372 		out.flush();
373 	}
374 
375 	/**
376 	 * JSTL function to return the tab state of the tab from the form.
377 	 * 
378 	 * @param form
379 	 * @param tabKey
380 	 * @return
381 	 */
382 	public static String getTabState(KualiForm form, String tabKey) {
383 		return form.getTabState(tabKey);
384 	}
385 
386 	public static void incrementTabIndex(KualiForm form, String tabKey) {
387 		form.incrementTabIndex();
388 	}
389 
390     /**
391      * Attempts to reopen sub tabs which would have been closed for inactive records
392      *
393      * @param sections the list of Sections whose rows and fields to set the open tab state on
394      * @param tabStates the map of tabKey->tabState.  This map will be modified to set entries to "OPEN"
395      * @param collectionName the name of the collection reopening
396      */
397     public static void reopenInactiveRecords(List<Section> sections, Map<String, String> tabStates, String collectionName) {
398         for (Section section : sections) {
399             for (Row row: section.getRows()) {
400                 for (Field field : row.getFields()) {
401                     if (field != null) {
402                         if (Field.CONTAINER.equals(field.getFieldType()) && StringUtils.startsWith(field.getContainerName(), collectionName)) {
403                             final String tabKey = WebUtils.generateTabKey(FieldUtils.generateCollectionSubTabName(field));
404                             tabStates.put(tabKey, KualiForm.TabState.OPEN.name());
405                         }
406                     }
407                 }
408             }
409         }
410     }
411 
412 	/**
413 	 * Generates a String from the title that can be used as a Map key.
414 	 * 
415 	 * @param tabTitle
416 	 * @return
417 	 */
418 	public static String generateTabKey(String tabTitle) {
419 		String key = "";
420 		if (!StringUtils.isBlank(tabTitle)) {
421 			key = tabTitle.replaceAll("\\W", "");
422 			// if (key.length() > 25) {
423 			// key = key.substring(0, 24);
424 			// }
425 		}
426 
427 		return key;
428 	}
429 
430 	public static void getMultipartParameters(HttpServletRequest request, ActionServletWrapper servletWrapper,
431 			ActionForm form, ActionMapping mapping) {
432 		Map params = new HashMap();
433 
434 		// Get the ActionServletWrapper from the form bean
435 		// ActionServletWrapper servletWrapper = getServletWrapper();
436 
437 		try {
438 			CommonsMultipartRequestHandler multipartHandler = new CommonsMultipartRequestHandler();
439 			if (multipartHandler != null) {
440 				// Set servlet and mapping info
441 				if (servletWrapper != null) {
442 					// from pojoformbase
443 					// servlet only affects tempdir on local disk
444 					servletWrapper.setServletFor(multipartHandler);
445 				}
446 				multipartHandler.setMapping((ActionMapping) request.getAttribute(Globals.MAPPING_KEY));
447 				// Initialize multipart request class handler
448 				multipartHandler.handleRequest(request);
449 
450 				Collection<FormFile> files = multipartHandler.getFileElements().values();
451 				Enumeration keys = multipartHandler.getFileElements().keys();
452 
453 				while (keys.hasMoreElements()) {
454 					Object key = keys.nextElement();
455 					FormFile file = (FormFile) multipartHandler.getFileElements().get(key);
456 					long maxSize = WebUtils.getMaxUploadSize(form);
457 					if (LOG.isDebugEnabled()) {
458 						LOG.debug(file.getFileSize());
459 					}
460 					if (maxSize > 0 && Long.parseLong(file.getFileSize() + "") > maxSize) {
461 
462 						GlobalVariables.getMessageMap().putError(key.toString(),
463 								RiceKeyConstants.ERROR_UPLOADFILE_SIZE,
464 								new String[] { file.getFileName(), Long.toString(maxSize) });
465 
466 					}
467 				}
468 
469 				// get file elements for kualirequestprocessor
470 				if (servletWrapper == null) {
471 					request.setAttribute(KRADConstants.UPLOADED_FILE_REQUEST_ATTRIBUTE_KEY,
472 							getFileParametersForMultipartRequest(request, multipartHandler));
473 				}
474 			}
475 		}
476 		catch (ServletException e) {
477 			throw new ValidationException("unable to handle multipart request " + e.getMessage(), e);
478 		}
479 	}
480 
481 	public static long getMaxUploadSize(ActionForm form) {
482 		long max = 0L;
483 		KualiMultipartRequestHandler multipartHandler = new KualiMultipartRequestHandler();
484 		if (form instanceof PojoFormBase) {
485 			max = multipartHandler.calculateMaxUploadSizeToMaxOfList(((PojoFormBase) form).getMaxUploadSizes());
486 		}
487 		if (LOG.isDebugEnabled()) {
488 			LOG.debug("Max File Upload Size: " + max);
489 		}
490 		return max;
491 	}
492 
493 	private static Map getFileParametersForMultipartRequest(HttpServletRequest request,
494 			MultipartRequestHandler multipartHandler) {
495 		Map parameters = new HashMap();
496 		Hashtable elements = multipartHandler.getFileElements();
497 		Enumeration e = elements.keys();
498 		while (e.hasMoreElements()) {
499 			String key = (String) e.nextElement();
500 			parameters.put(key, elements.get(key));
501 		}
502 
503 		if (request instanceof MultipartRequestWrapper) {
504 			request = (HttpServletRequest) ((MultipartRequestWrapper) request).getRequest();
505 			e = request.getParameterNames();
506 			while (e.hasMoreElements()) {
507 				String key = (String) e.nextElement();
508 				parameters.put(key, request.getParameterValues(key));
509 			}
510 		}
511 		else {
512 			LOG.debug("Gathering multipart parameters for unwrapped request");
513 		}
514 		return parameters;
515 	}
516 
517 	// end multipart
518 
519 	public static void registerEditableProperty(PojoFormBase form, String editablePropertyName) {
520 		form.registerEditableProperty(editablePropertyName);
521 	}
522 
523 	public static boolean isDocumentSession(Document document, PojoFormBase docForm) {
524 		boolean sessionDoc = document instanceof org.kuali.rice.krad.document.SessionDocument;
525 		boolean dataDictionarySessionDoc = false;
526 		if (!sessionDoc) {
527 			DataDictionary dataDictionary = KRADServiceLocatorWeb.getDataDictionaryService().getDataDictionary();
528 			if (docForm instanceof KualiMaintenanceForm) {
529 				KualiMaintenanceForm maintenanceForm = (KualiMaintenanceForm) docForm;
530 				if (dataDictionary != null) {
531 					if (maintenanceForm.getDocTypeName() != null) {
532                         MaintenanceDocumentEntry maintenanceDocumentEntry = (MaintenanceDocumentEntry) dataDictionary.getDocumentEntry(maintenanceForm.getDocTypeName());
533 						dataDictionarySessionDoc = maintenanceDocumentEntry.isSessionDocument();
534 					}
535 				}
536 			}
537 			else {
538 				if (document != null && dataDictionary != null) {
539 					KNSDocumentEntry documentEntry = (KNSDocumentEntry) dataDictionary.getDocumentEntry(document.getClass().getName());
540 					dataDictionarySessionDoc = documentEntry.isSessionDocument();
541 				}
542 			}
543 		}
544 		return sessionDoc || dataDictionarySessionDoc;
545 	}
546 
547 	public static boolean isFormSessionDocument(PojoFormBase form) {
548 		Document document = null;
549 		if (KualiDocumentFormBase.class.isAssignableFrom(form.getClass())) {
550 			KualiDocumentFormBase docForm = (KualiDocumentFormBase) form;
551 			document = docForm.getDocument();
552 		}
553 		return isDocumentSession(document, form);
554 	}
555 
556 	public static String KEY_KUALI_FORM_IN_SESSION = "KualiForm";
557 
558 	public static ActionForm getKualiForm(PageContext pageContext) {
559 		return getKualiForm((HttpServletRequest) pageContext.getRequest());
560 	}
561 
562 	public static ActionForm getKualiForm(HttpServletRequest request) {
563 		if (request.getAttribute(KEY_KUALI_FORM_IN_SESSION) != null) {
564 			return (ActionForm) request.getAttribute(KEY_KUALI_FORM_IN_SESSION);
565 		}
566 		else {
567 			final HttpSession session = request.getSession(false);
568 			return session != null ? (ActionForm) session.getAttribute(KEY_KUALI_FORM_IN_SESSION) : null;
569 		}
570 	}
571 
572 	public static boolean isPropertyEditable(Set<String> editableProperties, String propertyName) {
573 		if (LOG.isDebugEnabled()) {
574 			LOG.debug("isPropertyEditable(" + propertyName + ")");
575 		}
576 
577 		boolean returnVal = editableProperties == null
578 				|| editableProperties.contains(propertyName)
579 				|| (getIndexOfCoordinateExtension(propertyName) == -1 ? false : editableProperties
580 						.contains(propertyName.substring(0, getIndexOfCoordinateExtension(propertyName))));
581 		if (!returnVal) {
582 			if (LOG.isDebugEnabled()) {
583 				LOG.debug("isPropertyEditable(" + propertyName + ") == false / editableProperties: "
584 						+ editableProperties);
585 			}
586 		}
587 		return returnVal;
588 	}
589 
590 	public static boolean endsWithCoordinates(String parameter) {
591 		return parameter.endsWith(WebUtils.IMAGE_COORDINATE_CLICKED_X_EXTENSION)
592 				|| parameter.endsWith(WebUtils.IMAGE_COORDINATE_CLICKED_Y_EXTENSION);
593 	}
594 
595 	public static int getIndexOfCoordinateExtension(String parameter) {
596 		int indexOfCoordinateExtension = parameter.lastIndexOf(WebUtils.IMAGE_COORDINATE_CLICKED_X_EXTENSION);
597         if (indexOfCoordinateExtension == -1) {
598             indexOfCoordinateExtension = parameter.lastIndexOf(WebUtils.IMAGE_COORDINATE_CLICKED_Y_EXTENSION);
599         }
600 		return indexOfCoordinateExtension;
601 	}
602 
603     public static boolean isInquiryHiddenField(String className, String fieldName, Object formObject, String propertyName) {
604     	boolean isHidden = false;
605     	String hiddenInquiryFields = getKualiConfigurationService().getPropertyValueAsString(className + ".hidden");
606     	if (StringUtils.isEmpty(hiddenInquiryFields)) {
607     		return isHidden;
608     	}
609     	List hiddenFields = Arrays.asList(hiddenInquiryFields.replaceAll(" ", "").split(","));
610     	if (hiddenFields.contains(fieldName.trim())) {
611     		isHidden = true;
612     	}
613     	return isHidden;
614     }
615 
616     public static boolean isHiddenKimObjectType(String type, String configParameter) {
617     	boolean hideType = false;
618     	String hiddenTypes = getKualiConfigurationService().getPropertyValueAsString(configParameter);
619     	if (StringUtils.isEmpty(hiddenTypes)) {
620     		return hideType;
621     	}
622     	List hiddenTypeValues = Arrays.asList(hiddenTypes.replaceAll(" ", "").split(","));
623     	if (hiddenTypeValues.contains(type.trim())) {
624     		hideType = true;
625     	}
626     	return hideType;
627     }
628 
629 	public static String getFullyMaskedValue(String className, String fieldName, Object formObject, String propertyName) {
630 		String displayMaskValue = null;
631 		Object propertyValue = ObjectUtils.getPropertyValue(formObject, propertyName);
632 
633 		DataDictionaryEntryBase entry = (DataDictionaryEntryBase) KRADServiceLocatorWeb.getDataDictionaryService()
634 				.getDataDictionary().getDictionaryObjectEntry(className);
635 		AttributeDefinition a = entry.getAttributeDefinition(fieldName);
636 
637 		AttributeSecurity attributeSecurity = a.getAttributeSecurity();
638 		if (attributeSecurity != null && attributeSecurity.isMask()) {
639 			MaskFormatter maskFormatter = attributeSecurity.getMaskFormatter();
640 			displayMaskValue = maskFormatter.maskValue(propertyValue);
641 
642 		}
643 		return displayMaskValue;
644 	}
645 
646 	public static String getPartiallyMaskedValue(String className, String fieldName, Object formObject,
647 			String propertyName) {
648 		String displayMaskValue = null;
649 		Object propertyValue = ObjectUtils.getPropertyValue(formObject, propertyName);
650 
651 		DataDictionaryEntryBase entry = (DataDictionaryEntryBase) KRADServiceLocatorWeb.getDataDictionaryService()
652 				.getDataDictionary().getDictionaryObjectEntry(className);
653 		AttributeDefinition a = entry.getAttributeDefinition(fieldName);
654 
655 		AttributeSecurity attributeSecurity = a.getAttributeSecurity();
656 		if (attributeSecurity != null && attributeSecurity.isPartialMask()) {
657 			MaskFormatter partialMaskFormatter = attributeSecurity.getPartialMaskFormatter();
658 			displayMaskValue = partialMaskFormatter.maskValue(propertyValue);
659 
660 		}
661 		return displayMaskValue;
662 	}
663 
664 	public static boolean canFullyUnmaskField(String businessObjectClassName, String fieldName, KualiForm form) {
665 		Class businessObjClass = null;
666 		try {
667 			businessObjClass = Class.forName(businessObjectClassName);
668 		}
669 		catch (Exception e) {
670 			throw new RuntimeException("Unable to resolve class name: " + businessObjectClassName);
671 		}
672 		if (form instanceof KualiDocumentFormBase) {
673 			return KNSServiceLocator.getBusinessObjectAuthorizationService().canFullyUnmaskField(
674 					GlobalVariables.getUserSession().getPerson(), businessObjClass, fieldName,
675 					((KualiDocumentFormBase) form).getDocument());
676 		}
677 		else {
678 			return KNSServiceLocator.getBusinessObjectAuthorizationService().canFullyUnmaskField(
679 					GlobalVariables.getUserSession().getPerson(), businessObjClass, fieldName, null);
680 		}
681 	}
682 
683 	public static boolean canPartiallyUnmaskField(String businessObjectClassName, String fieldName, KualiForm form) {
684 		Class businessObjClass = null;
685 		try {
686 			businessObjClass = Class.forName(businessObjectClassName);
687 		}
688 		catch (Exception e) {
689 			throw new RuntimeException("Unable to resolve class name: " + businessObjectClassName);
690 		}
691 		if (form instanceof KualiDocumentFormBase) {
692 			return KNSServiceLocator.getBusinessObjectAuthorizationService().canPartiallyUnmaskField(
693 					GlobalVariables.getUserSession().getPerson(), businessObjClass, fieldName,
694 					((KualiDocumentFormBase) form).getDocument());
695 		}
696 		else {
697 			return KNSServiceLocator.getBusinessObjectAuthorizationService().canPartiallyUnmaskField(
698 					GlobalVariables.getUserSession().getPerson(), businessObjClass, fieldName, null);
699 		}
700 	}
701 
702 	public static boolean canAddNoteAttachment(Document document) {
703 		boolean canViewNoteAttachment = false;
704 		DocumentAuthorizer documentAuthorizer = KNSServiceLocator.getDocumentHelperService().getDocumentAuthorizer(
705 				document);
706 		canViewNoteAttachment = documentAuthorizer.canAddNoteAttachment(document, null, GlobalVariables
707 				.getUserSession().getPerson());
708 		return canViewNoteAttachment;
709 	}
710 
711 	public static boolean canViewNoteAttachment(Document document, String attachmentTypeCode) {
712 		boolean canViewNoteAttachment = false;
713 		DocumentAuthorizer documentAuthorizer = KNSServiceLocator.getDocumentHelperService().getDocumentAuthorizer(
714 				document);
715 		canViewNoteAttachment = documentAuthorizer.canViewNoteAttachment(document, attachmentTypeCode, GlobalVariables
716 				.getUserSession().getPerson());
717 		return canViewNoteAttachment;
718 	}
719 
720 	public static boolean canDeleteNoteAttachment(Document document, String attachmentTypeCode,
721 			String authorUniversalIdentifier) {
722 		boolean canDeleteNoteAttachment = false;
723 		DocumentAuthorizer documentAuthorizer = KNSServiceLocator.getDocumentHelperService().getDocumentAuthorizer(
724 				document);
725 		canDeleteNoteAttachment = documentAuthorizer.canDeleteNoteAttachment(document, attachmentTypeCode, "false",
726 				GlobalVariables.getUserSession().getPerson());
727 		if (canDeleteNoteAttachment) {
728 			return canDeleteNoteAttachment;
729 		}
730 		else {
731 			canDeleteNoteAttachment = documentAuthorizer.canDeleteNoteAttachment(document, attachmentTypeCode, "true",
732 					GlobalVariables.getUserSession().getPerson());
733 			if (canDeleteNoteAttachment
734 					&& !authorUniversalIdentifier.equals(GlobalVariables.getUserSession().getPerson().getPrincipalId())) {
735 				canDeleteNoteAttachment = false;
736 			}
737 		}
738 		return canDeleteNoteAttachment;
739 	}
740 
741 	public static void reuseErrorMapFromPreviousRequest(KualiDocumentFormBase kualiDocumentFormBase) {
742 		if (kualiDocumentFormBase.getMessageMapFromPreviousRequest() == null) {
743 			LOG.error("Error map from previous request is null!");
744 			return;
745 		}
746 		MessageMap errorMapFromGlobalVariables = GlobalVariables.getMessageMap();
747 		if (kualiDocumentFormBase.getMessageMapFromPreviousRequest() == errorMapFromGlobalVariables) {
748 			// if we've switched them already, then return early and do nothing
749 			return;
750 		}
751 		if (!errorMapFromGlobalVariables.hasNoErrors()) {
752 			throw new RuntimeException("Cannot replace error map because it is not empty");
753 		}
754 		GlobalVariables.setMessageMap(kualiDocumentFormBase.getMessageMapFromPreviousRequest());
755 		GlobalVariables.getMessageMap().clearErrorPath();
756 	}
757 
758 	/**
759 	 * Excapes out HTML to prevent XSS attacks, and replaces the following
760 	 * strings to allow for a limited set of HTML tags
761 	 * 
762 	 * <li>[X] and [/X], where X represents any 1 or 2 letter string may be used
763 	 * to specify the equivalent tag in HTML (i.e. &lt;X&gt; and &lt;/X&gt;) <li>
764 	 * [font COLOR], where COLOR represents any valid html color (i.e. color
765 	 * name or hexcode preceeded by #) will be filtered into &lt;font
766 	 * color="COLOR"/&gt; <li>[/font] will be filtered into &lt;/font&gt; <li>
767 	 * [table CLASS], where CLASS gives the style class to use, will be filter
768 	 * into &lt;table class="CLASS"/&gt; <li>[/table] will be filtered into
769 	 * &lt;/table&gt; <li>[td CLASS], where CLASS gives the style class to use,
770 	 * will be filter into &lt;td class="CLASS"/&gt;
771 	 * 
772 	 * @param inputString
773 	 * @return
774 	 */
775 	public static String filterHtmlAndReplaceRiceMarkup(String inputString) {
776 		String outputString = StringEscapeUtils.escapeHtml(inputString);
777 		// string has been escaped of all <, >, and & (and other characters)
778 
779         Map<String, String> findAndReplacePatterns = new LinkedHashMap<String, String>();
780 
781         // now replace our rice custom markup into html
782 
783         // DON'T ALLOW THE SCRIPT TAG OR ARBITRARY IMAGES/URLS/ETC. THROUGH
784 
785         //strip out instances where javascript precedes a URL
786         findAndReplacePatterns.put("\\[a ((javascript|JAVASCRIPT|JavaScript).+)\\]", "");
787         //turn passed a href value into appropriate tag
788         findAndReplacePatterns.put("\\[a (.+)\\]", "<a href=\"$1\">");
789         findAndReplacePatterns.put("\\[/a\\]", "</a>");
790         
791 		// filter any one character tags
792 		findAndReplacePatterns.put("\\[([A-Za-z])\\]", "<$1>");
793 		findAndReplacePatterns.put("\\[/([A-Za-z])\\]", "</$1>");
794 		// filter any two character tags
795 		findAndReplacePatterns.put("\\[([A-Za-z]{2})\\]", "<$1>");
796 		findAndReplacePatterns.put("\\[/([A-Za-z]{2})\\]", "</$1>");
797 		// filter the font tag
798 		findAndReplacePatterns.put("\\[font (#[0-9A-Fa-f]{1,6}|[A-Za-z]+)\\]", "<font color=\"$1\">");
799 		findAndReplacePatterns.put("\\[/font\\]", "</font>");
800 		// filter the table tag
801 		findAndReplacePatterns.put("\\[table\\]", "<table>");
802 		findAndReplacePatterns.put("\\[table ([A-Za-z]+)\\]", "<table class=\"$1\">");
803 		findAndReplacePatterns.put("\\[/table\\]", "</table>");
804 		// fiter td with class
805 		findAndReplacePatterns.put("\\[td ([A-Za-z]+)\\]", "<td class=\"$1\">");
806 
807 		for (String findPattern : findAndReplacePatterns.keySet()) {
808 			Pattern p = Pattern.compile(findPattern);
809 			Matcher m = p.matcher(outputString);
810 			if (m.find()) {
811 				String replacePattern = findAndReplacePatterns.get(findPattern);
812 				outputString = m.replaceAll(replacePattern);
813 			}
814 		}
815 
816 		return outputString;
817 	}
818 
819     /**
820 	 * Determines and returns the URL for question button images; looks first
821 	 * for a property "application.custom.image.url", and if that is missing,
822 	 * uses the image url returned by getDefaultButtonImageUrl()
823 	 * 
824 	 * @param imageName
825 	 *            the name of the image to find a button for
826 	 * @return the URL where question button images are located
827 	 */
828 	public static String getButtonImageUrl(String imageName) {
829 		String buttonImageUrl = getKualiConfigurationService().getPropertyValueAsString(
830                 WebUtils.APPLICATION_IMAGE_URL_PROPERTY_PREFIX + "." + imageName);
831 		if (StringUtils.isBlank(buttonImageUrl)) {
832 			buttonImageUrl = getDefaultButtonImageUrl(imageName);
833 		}
834 		return buttonImageUrl;
835 	}
836 
837     public static String getAttachmentImageForUrl(String contentType) {
838         String image = getKualiConfigurationService().getPropertyValueAsString(KRADConstants.ATTACHMENT_IMAGE_PREFIX + contentType);
839         if (StringUtils.isEmpty(image)) {
840             return getKualiConfigurationService().getPropertyValueAsString(KRADConstants.ATTACHMENT_IMAGE_DEFAULT);
841         }
842         return image;
843     }
844 
845 	/**
846 	 * Generates a default button image URL, in the form of:
847 	 * ${kr.externalizable.images.url}buttonsmall_${imageName}.gif
848 	 * 
849 	 * @param imageName
850 	 *            the image name to generate a default button name for
851 	 * @return the default button image url
852 	 */
853 	public static String getDefaultButtonImageUrl(String imageName) {
854 		return getKualiConfigurationService().getPropertyValueAsString(WebUtils.DEFAULT_IMAGE_URL_PROPERTY_NAME)
855 				+ "buttonsmall_" + imageName + ".gif";
856 	}
857 
858     /**
859 	 * @return an implementation of the KualiConfigurationService
860 	 */
861 	public static ConfigurationService getKualiConfigurationService() {
862 		if (configurationService == null) {
863 			configurationService = KRADServiceLocator.getKualiConfigurationService();
864 		}
865 		return configurationService;
866 	}
867 	
868     /**
869      * Takes a string an converts the whitespace which would be ignored in an
870      * HTML document into HTML elements so the whitespace is preserved
871      * 
872      * @param startingString The string to preserve whitespace in
873      * @return A string whose whitespace has been converted to HTML elements to preserve the whitespace in an HTML document
874      */
875     public static String preserveWhitespace(String startingString) {
876     	String convertedString = startingString.replaceAll("\n", "<br />");
877     	convertedString = convertedString.replaceAll("  ", "&nbsp;&nbsp;").replaceAll("(&nbsp; | &nbsp;)", "&nbsp;&nbsp;");
878     	return convertedString;
879     }
880     
881     public static String getKimGroupDisplayName(String groupId) {
882     	if(StringUtils.isBlank(groupId)) {
883     		throw new IllegalArgumentException("Group ID must have a value");
884     	}
885     	return KimApiServiceLocator.getGroupService().getGroup(groupId).getName();
886     }
887     
888     public static String getPrincipalDisplayName(String principalId) {
889     	if(StringUtils.isBlank(principalId)) {
890     		throw new IllegalArgumentException("Principal ID must have a value");
891     	}
892         if (KimApiServiceLocator.getIdentityService().getDefaultNamesForPrincipalId(principalId) == null){
893             return "";
894         }
895         else {
896     	    return KimApiServiceLocator.getIdentityService().getDefaultNamesForPrincipalId(principalId).getDefaultName().getCompositeName();
897         }
898     }
899 
900     /**
901      * Takes an {@link org.kuali.rice.kew.api.action.ActionRequest} with a recipient type of
902      * {@link org.kuali.rice.kew.api.action.RecipientType#ROLE} and returns the display name for the role.
903      *
904      * @param actionRequest the action request
905      * @return the display name for the role
906      * @throws IllegalArgumentException if the action request is null, or the recipient type is not ROLE
907      */
908     public static String getRoleDisplayName(ActionRequest actionRequest) {
909         String result;
910 
911         if(actionRequest == null) {
912             throw new IllegalArgumentException("actionRequest must be non-null");
913         }
914         if (RecipientType.ROLE != actionRequest.getRecipientType()) {
915             throw new IllegalArgumentException("actionRequest recipient must be a Role");
916         }
917 
918         Role role = KimApiServiceLocator.getRoleService().getRole(actionRequest.getRoleName());
919 
920         if (role != null) {
921             result = role.getName();
922         } else if (!StringUtils.isBlank(actionRequest.getQualifiedRoleNameLabel())) {
923             result = actionRequest.getQualifiedRoleNameLabel();
924         } else {
925             result = actionRequest.getRoleName();
926         }
927 
928         return result;
929     }
930 
931     /**
932      * Returns an absolute URL which is a combination of a base part plus path,
933      * or in the case that the path is already an absolute URL, the path alone
934      * @param base the url base path
935      * @param path the path to append to base
936      * @return an absolute URL representing the combination of base+path, or path alone if it is already absolute
937      */
938     public static String toAbsoluteURL(String base, String path) {
939         boolean abs = false;
940         if (StringUtils.isBlank(path)) {
941             path = "";
942         } else {
943             for (String scheme: SCHEMES) {
944                 if (path.startsWith(scheme)) {
945                     abs = true;
946                     break;
947                 }
948             }
949         }
950         if (abs) {
951             return path;
952         }
953         return base + path;
954     }
955 
956 }