View Javadoc
1   /**
2    * Copyright 2005-2015 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.krad.util;
17  
18  import java.io.IOException;
19  import java.io.InputStream;
20  import java.lang.annotation.Annotation;
21  import java.lang.reflect.Constructor;
22  import java.lang.reflect.InvocationTargetException;
23  import java.lang.reflect.Method;
24  import java.net.MalformedURLException;
25  import java.net.URL;
26  import java.security.GeneralSecurityException;
27  import java.text.MessageFormat;
28  import java.text.NumberFormat;
29  import java.text.ParseException;
30  import java.util.ArrayList;
31  import java.util.Arrays;
32  import java.util.Collection;
33  import java.util.Collections;
34  import java.util.HashMap;
35  import java.util.Iterator;
36  import java.util.List;
37  import java.util.Map;
38  import java.util.Properties;
39  import java.util.regex.Pattern;
40  
41  import javax.servlet.ServletRequest;
42  import javax.servlet.http.HttpServletRequest;
43  import javax.servlet.http.HttpServletResponse;
44  
45  import org.apache.commons.beanutils.PropertyUtils;
46  import org.apache.commons.codec.EncoderException;
47  import org.apache.commons.codec.net.URLCodec;
48  import org.apache.commons.lang.StringUtils;
49  import org.apache.log4j.Logger;
50  import org.kuali.rice.core.api.CoreApiServiceLocator;
51  import org.kuali.rice.core.api.encryption.EncryptionService;
52  import org.kuali.rice.core.api.search.SearchOperator;
53  import org.kuali.rice.core.api.util.RiceKeyConstants;
54  import org.kuali.rice.core.api.util.Truth;
55  import org.kuali.rice.core.api.util.type.KualiDecimal;
56  import org.kuali.rice.core.api.util.type.TypeUtils;
57  import org.kuali.rice.core.web.format.BooleanFormatter;
58  import org.kuali.rice.core.web.format.FormatException;
59  import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
60  import org.kuali.rice.coreservice.framework.parameter.ParameterConstants;
61  import org.kuali.rice.coreservice.framework.parameter.ParameterService;
62  import org.kuali.rice.krad.UserSession;
63  import org.kuali.rice.krad.data.KradDataServiceLocator;
64  import org.kuali.rice.krad.messages.MessageService;
65  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
66  import org.kuali.rice.krad.service.KualiModuleService;
67  import org.kuali.rice.krad.service.ModuleService;
68  import org.kuali.rice.krad.uif.UifConstants;
69  import org.kuali.rice.krad.uif.UifParameters;
70  import org.kuali.rice.krad.uif.component.ClientSideState;
71  import org.kuali.rice.krad.uif.component.Component;
72  import org.kuali.rice.krad.uif.element.Action;
73  import org.kuali.rice.krad.uif.element.Image;
74  import org.kuali.rice.krad.uif.element.Link;
75  import org.kuali.rice.krad.uif.field.ActionField;
76  import org.kuali.rice.krad.uif.field.DataField;
77  import org.kuali.rice.krad.uif.field.Field;
78  import org.kuali.rice.krad.uif.field.FieldGroup;
79  import org.kuali.rice.krad.uif.field.ImageField;
80  import org.kuali.rice.krad.uif.field.LinkField;
81  import org.kuali.rice.krad.uif.field.MessageField;
82  import org.kuali.rice.krad.uif.field.SpaceField;
83  import org.kuali.rice.krad.uif.util.CopyUtils;
84  import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
85  import org.kuali.rice.krad.uif.util.ViewModelUtils;
86  import org.kuali.rice.krad.uif.view.ExpressionEvaluator;
87  import org.kuali.rice.krad.uif.view.View;
88  import org.kuali.rice.krad.web.form.UifFormBase;
89  import org.springframework.beans.PropertyAccessorUtils;
90  import org.springframework.util.Assert;
91  import org.springframework.util.FileCopyUtils;
92  
93  /**
94   * Miscellaneous Utility Methods.
95   *
96   * @author Kuali Rice Team (rice.collab@kuali.org)
97   */
98  public final class KRADUtils {
99      private static final Logger LOG = Logger.getLogger(KRADUtils.class);
100 
101     private static KualiModuleService kualiModuleService;
102     private static final KualiDecimal ONE_HUNDRED = new KualiDecimal("100.00");
103 
104     /**
105      * Prevent instantiation of the class.
106      */
107     private KRADUtils() {
108         throw new UnsupportedOperationException("do not call");
109     }
110 
111     /**
112      * Retrieve the title for a business object class
113      *
114      * <p>
115      * The title is a nicely formatted version of the simple class name.
116      * </p>
117      *
118      * @param clazz business object class
119      * @return title of the business object class
120      */
121     public final static String getBusinessTitleForClass(Class<? extends Object> clazz) {
122         if (clazz == null) {
123             throw new IllegalArgumentException(
124                     "The getBusinessTitleForClass method of KRADUtils requires a non-null class");
125         }
126         String className = clazz.getSimpleName();
127 
128         StringBuffer label = new StringBuffer(className.substring(0, 1));
129         for (int i = 1; i < className.length(); i++) {
130             if (Character.isLowerCase(className.charAt(i))) {
131                 label.append(className.charAt(i));
132             } else {
133                 label.append(" ").append(className.charAt(i));
134             }
135         }
136         return label.toString().trim();
137     }
138 
139     /**
140      * Picks off the filename from the full path
141      *
142      * <p>
143      * The different OS path separators are being taken into consideration.
144      * </p>
145      *
146      * @param fullFileNames file name with path
147      * @return file name
148      */
149     public final static List<String> getFileNameFromPath(List<String> fullFileNames) {
150         List<String> fileNameList = new ArrayList<String>();
151 
152         for (String fullFileName : fullFileNames) {
153             if (StringUtils.contains(fullFileName, "/")) {
154                 fileNameList.add(StringUtils.substringAfterLast(fullFileName, "/"));
155             } else {
156                 fileNameList.add(StringUtils.substringAfterLast(fullFileName, "\\"));
157             }
158         }
159 
160         return fileNameList;
161     }
162 
163     /**
164      * Convert the given money amount into an integer string.
165      *
166      * <p>
167      * Since the return string cannot have decimal point, multiplies the amount by 100 so the decimal places
168      * are not lost, for example, 320.15 is converted into 32015.
169      * </p>
170      *
171      * @param decimalNumber decimal number to be converted
172      * @return an integer string of the given money amount through multiplying by 100 and removing the fraction
173      * portion.
174      */
175     public final static String convertDecimalIntoInteger(KualiDecimal decimalNumber) {
176         KualiDecimal decimalAmount = decimalNumber.multiply(ONE_HUNDRED);
177         NumberFormat formatter = NumberFormat.getIntegerInstance();
178         String formattedAmount = formatter.format(decimalAmount);
179 
180         return StringUtils.replace(formattedAmount, ",", "");
181     }
182 
183     /**
184      * Return the integer value of a string
185      *
186      * <p>
187      * If the string contains a decimal value everything after the decimal point is dropped.
188      * </p>
189      *
190      * @param numberStr string
191      * @return integer representation of the given string
192      */
193     public static Integer getIntegerValue(String numberStr) {
194         Integer numberInt = null;
195         try {
196             numberInt = new Integer(numberStr);
197         } catch (NumberFormatException nfe) {
198             Double numberDbl = new Double(numberStr);
199             numberInt = new Integer(numberDbl.intValue());
200         }
201         return numberInt;
202     }
203 
204     /**
205      * Attempt to coerce a String attribute value to the given propertyType.  If the transformation can't be made,
206      * either because the propertyType is null or because the transformation required exceeds this method's very small
207      * bag of tricks, then null is returned.
208      *
209      * @param propertyType the Class to coerce the attributeValue to
210      * @param attributeValue the String value to coerce
211      * @return an instance of the propertyType class, or null the transformation can't be made.
212      */
213     public static Object hydrateAttributeValue(Class<?> propertyType, String attributeValue) {
214         Object attributeValueObject = null;
215         if (propertyType != null && attributeValue != null) {
216             if (String.class.equals(propertyType)) {
217                 // it's already a String
218                 attributeValueObject = attributeValue;
219             } // KULRICE-6808: Kim Role Maintenance - Custom boolean role qualifier values are not being converted properly
220             else if (Boolean.class.equals(propertyType) || Boolean.TYPE.equals(propertyType)) {
221                 attributeValueObject = Truth.strToBooleanIgnoreCase(attributeValue);
222             } else {
223                 // try to create one with KRADUtils for other misc data types
224                 attributeValueObject = KRADUtils.createObject(propertyType, new Class[]{String.class},
225                         new Object[]{attributeValue});
226                 // if that didn't work, we'll get a null back
227             }
228         }
229         return attributeValueObject;
230     }
231 
232     public static Object createObject(Class<?> clazz, Class<?>[] argumentClasses, Object[] argumentValues) {
233         if (clazz == null) {
234             return null;
235         }
236         if (argumentClasses.length == 1 && argumentClasses[0] == String.class) {
237             if (argumentValues.length == 1 && argumentValues[0] != null) {
238                 if (clazz == String.class) {
239                     // this means we're trying to create a String from a String
240                     // don't new up Strings, it's a bad idea
241                     return argumentValues[0];
242                 } else {
243                     // maybe it's a type that supports valueOf?
244                     Method valueOfMethod = null;
245                     try {
246                         valueOfMethod = clazz.getMethod("valueOf", String.class);
247                     } catch (NoSuchMethodException e) {
248                         // ignored
249                     }
250                     if (valueOfMethod != null) {
251                         try {
252                             return valueOfMethod.invoke(null, argumentValues[0]);
253                         } catch (Exception e) {
254                             // ignored
255                         }
256                     }
257                 }
258             }
259         }
260         try {
261             Constructor<?> constructor = clazz.getConstructor(argumentClasses);
262             return constructor.newInstance(argumentValues);
263         } catch (Exception e) {
264             // ignored
265         }
266         return null;
267     }
268 
269     /**
270      * Creates a comma separated String representation of the given list.
271      *
272      * <p>
273      * For example 'a','b',c'.
274      * </p>
275      *
276      * @param list
277      * @return the joined String, empty if the list is null or has no elements
278      */
279     public static String joinWithQuotes(List<String> list) {
280         if (list == null || list.size() == 0) {
281             return "";
282         }
283 
284         return KRADConstants.SINGLE_QUOTE +
285                 StringUtils.join(list.iterator(), KRADConstants.SINGLE_QUOTE + "," + KRADConstants.SINGLE_QUOTE) +
286                 KRADConstants.SINGLE_QUOTE;
287     }
288 
289     /**
290      * TODO this method will probably need to be exposed in a public KRADUtils class as it is used
291      * by several different modules.  That will have to wait until ModuleService and KualiModuleService are moved
292      * to core though.
293      *
294      * @param clazz class to get a namespace code for
295      * @return namespace code
296      */
297     public static String getNamespaceCode(Class<? extends Object> clazz) {
298         ModuleService moduleService = getKualiModuleService().getResponsibleModuleService(clazz);
299         if (moduleService == null) {
300             return KRADConstants.DEFAULT_NAMESPACE;
301         }
302         return moduleService.getModuleConfiguration().getNamespaceCode();
303     }
304 
305     public static Map<String, String> getNamespaceAndComponentSimpleName(Class<? extends Object> clazz) {
306         Map<String, String> map = new HashMap<String, String>();
307         map.put(KRADConstants.NAMESPACE_CODE, getNamespaceCode(clazz));
308         map.put(KRADConstants.COMPONENT_NAME, getComponentSimpleName(clazz));
309         return map;
310     }
311 
312     public static Map<String, String> getNamespaceAndComponentFullName(Class<? extends Object> clazz) {
313         Map<String, String> map = new HashMap<String, String>();
314         map.put(KRADConstants.NAMESPACE_CODE, getNamespaceCode(clazz));
315         map.put(KRADConstants.COMPONENT_NAME, getComponentFullName(clazz));
316         return map;
317     }
318 
319     public static Map<String, String> getNamespaceAndActionClass(Class<? extends Object> clazz) {
320         Map<String, String> map = new HashMap<String, String>();
321         map.put(KRADConstants.NAMESPACE_CODE, getNamespaceCode(clazz));
322         map.put(KRADConstants.ACTION_CLASS, clazz.getName());
323         return map;
324     }
325 
326     private static String getComponentSimpleName(Class<? extends Object> clazz) {
327         return clazz.getSimpleName();
328     }
329 
330     private static String getComponentFullName(Class<? extends Object> clazz) {
331         return clazz.getName();
332     }
333 
334     /**
335      * Parses a string that is in map format (commas separating map entries, colon separates
336      * map key/value) to a new map instance
337      *
338      * @param parameter - string parameter to parse
339      * @return Map<String, String> instance populated from string parameter
340      */
341     public static Map<String, String> convertStringParameterToMap(String parameter) {
342         Map<String, String> map = new HashMap<String, String>();
343 
344         if (StringUtils.isNotBlank(parameter)) {
345             if (StringUtils.contains(parameter, ",")) {
346                 String[] fieldConversions = StringUtils.split(parameter, ",");
347 
348                 for (int i = 0; i < fieldConversions.length; i++) {
349                     String fieldConversionStr = fieldConversions[i];
350                     if (StringUtils.isNotBlank(fieldConversionStr)) {
351                         if (StringUtils.contains(fieldConversionStr, ":")) {
352                             String[] fieldConversion = fieldConversionStr.split(":",-1);
353                             map.put(fieldConversion[0], fieldConversion[1]);
354                         } else {
355                             map.put(fieldConversionStr, fieldConversionStr);
356                         }
357                     }
358                 }
359             } else if (StringUtils.contains(parameter, ":")) {
360                 String[] fieldConversion = parameter.split(":",-1);
361                 map.put(fieldConversion[0], fieldConversion[1]);
362             } else {
363                 map.put(parameter, parameter);
364             }
365         }
366 
367         return map;
368     }
369 
370     /**
371      * Parses a string that is in list format (commas separating list entries) to a new List instance
372      *
373      * @param parameter - string parameter to parse
374      * @return List<String> instance populated from string parameter
375      */
376     public static List<String> convertStringParameterToList(String parameter) {
377         List<String> list = new ArrayList<String>();
378 
379         if (StringUtils.isNotBlank(parameter)) {
380             if (StringUtils.contains(parameter, ",")) {
381                 String[] parameters = StringUtils.split(parameter, ",");
382                 List arraysList = Arrays.asList(parameters);
383                 list.addAll(arraysList);
384             } else {
385                 list.add(parameter);
386             }
387         }
388 
389         return list;
390     }
391 
392     /**
393      * Translates characters in the given string like brackets that will cause
394      * problems with binding to characters that do not affect the binding
395      *
396      * @param key - string to translate
397      * @return String translated string
398      */
399     public static String translateToMapSafeKey(String key) {
400         String safeKey = key;
401 
402         safeKey = StringUtils.replace(safeKey, "[", "_");
403         safeKey = StringUtils.replace(safeKey, "]", "_");
404 
405         return safeKey;
406     }
407 
408     /**
409      * Builds a string from the given map by joining each entry with a comma and
410      * each key/value pair with a colon
411      *
412      * @param map - map instance to build string for
413      * @return String of map entries
414      */
415     public static String buildMapParameterString(Map<String, String> map) {
416         String parameterString = "";
417 
418         for (Map.Entry<String, String> entry : map.entrySet()) {
419             if (StringUtils.isNotBlank(parameterString)) {
420                 parameterString += ",";
421             }
422 
423             parameterString += entry.getKey() + ":" + entry.getValue();
424         }
425 
426         return parameterString;
427     }
428 
429     /**
430      * Parses the given string into a Map by splitting on the comma to get the
431      * map entries and within each entry splitting by colon to get the key/value
432      * pairs
433      *
434      * @param parameterString - string to parse into map
435      * @return Map<String, String> map from string
436      */
437     public static Map<String, String> getMapFromParameterString(String parameterString) {
438         Map<String, String> map = new HashMap<String, String>();
439 
440         String[] entries = parameterString.split(",");
441         for (int i = 0; i < entries.length; i++) {
442             String[] keyValue = entries[i].split(":");
443             if (keyValue.length != 2) {
444                 throw new RuntimeException("malformed field conversion pair: " + Arrays.toString(keyValue));
445             }
446 
447             map.put(keyValue[0], keyValue[1]);
448         }
449 
450         return map;
451     }
452 
453     /**
454      * Retrieves value for the given parameter name in the request and attempts to convert to a Boolean using
455      * the <code>BooleanFormatter</code>
456      *
457      * @param request - servlet request containing parameters
458      * @param parameterName - name of parameter to retrieve value for
459      * @return Boolean set to value of parameter, or null if parameter was not found in request
460      */
461     public static Boolean getRequestParameterAsBoolean(ServletRequest request, String parameterName) {
462         Boolean parameterValue = null;
463 
464         String parameterValueStr = request.getParameter(parameterName);
465         if (StringUtils.isNotBlank(parameterValueStr)) {
466             parameterValue = (Boolean) new BooleanFormatter().convertFromPresentationFormat(parameterValueStr);
467         }
468 
469         return parameterValue;
470     }
471 
472     /**
473      * Translates the given Map of String keys and String array values to a Map
474      * of String key and values. If the String array contains more than one
475      * value, the single string is built by joining the values with the vertical
476      * bar character
477      *
478      * @param requestParameters - Map of request parameters to translate
479      * @return Map<String, String> translated Map
480      */
481     public static Map<String, String> translateRequestParameterMap(Map<String, String[]> requestParameters) {
482         Map<String, String> parameters = new HashMap<String, String>();
483 
484         for (Map.Entry<String, String[]> parameter : requestParameters.entrySet()) {
485             String parameterValue = "";
486             if (parameter.getValue().length > 1) {
487                 parameterValue = StringUtils.join(parameter.getValue(), "|");
488             } else {
489                 parameterValue = parameter.getValue()[0];
490             }
491             parameters.put(parameter.getKey(), parameterValue);
492         }
493 
494         return parameters;
495     }
496 
497     /**
498      * Retrieves parameter values from the request that match the requested
499      * names. In addition, based on the object class an authorization check is
500      * performed to determine if the values are secure and should be decrypted.
501      * If true, the value is decrypted before returning
502      *
503      * @param parameterNames - names of the parameters whose values should be retrieved
504      * from the request
505      * @param parentObjectClass - object class that contains the parameter names as properties
506      * and should be consulted for security checks
507      * @param requestParameters - all request parameters to pull from
508      * @return Map<String, String> populated with parameter name/value pairs
509      * pulled from the request
510      */
511     public static Map<String, String> getParametersFromRequest(List<String> parameterNames, Class<?> parentObjectClass,
512             Map<String, String> requestParameters) {
513         Map<String, String> parameterValues = new HashMap<String, String>();
514 
515         for (Iterator<String> iter = parameterNames.iterator(); iter.hasNext(); ) {
516             String keyPropertyName = iter.next();
517 
518             if (requestParameters.get(keyPropertyName) != null) {
519                 String keyValue = requestParameters.get(keyPropertyName);
520 
521                 // check security on field
522                 boolean isSecure = isSecure(keyPropertyName, parentObjectClass);
523 
524                 if (StringUtils.endsWith(keyValue, EncryptionService.ENCRYPTION_POST_PREFIX)) {
525                     keyValue = StringUtils.removeEnd(keyValue, EncryptionService.ENCRYPTION_POST_PREFIX);
526                     isSecure = true;
527                 }
528 
529                 // decrypt if the value is secure
530                 if (isSecure) {
531                     try {
532                         if (CoreApiServiceLocator.getEncryptionService().isEnabled()) {
533                             keyValue = CoreApiServiceLocator.getEncryptionService().decrypt(keyValue);
534                         }
535                     } catch (GeneralSecurityException e) {
536                         String message = "Data object class " + parentObjectClass + " property " + keyPropertyName
537                                 + " should have been encrypted, but there was a problem decrypting it.";
538                         LOG.error(message);
539 
540                         throw new RuntimeException(message, e);
541                     }
542                 }
543 
544                 parameterValues.put(keyPropertyName, keyValue);
545             }
546         }
547 
548         return parameterValues;
549     }
550 
551     /**
552      * Returns a map containing a key/value pair for each property given in the property names list, ignoring any values
553      * considered to be secure.
554      *
555      * @param propertyNames the list of property names for which to get key/value pairs
556      * @param dataObject the object instance containing the properties for which the values will be pulled
557      *
558      * @return a map containing an entry for each property name with the property name as the map key
559      * and the property value as the value
560      */
561     public static Map<String, String> getPropertyKeyValuesFromDataObject(List<String> propertyNames, Object dataObject) {
562         return getPropertyKeyValuesFromDataObject(propertyNames, Collections.<String>emptyList(), dataObject);
563     }
564 
565     /**
566      * Returns a map containing a key/value pair for each property given in the property names list, ignoring any values
567      * considered to be secure, including those in {@code securePropertyNames}.
568      *
569      * @param propertyNames the list of property names for which to get key/value pairs
570      * @param securePropertyNames list of additional secure property names to match for encryption
571      * @param dataObject the object instance containing the properties for which the values will be pulled
572      *
573      * @return a map containing an entry for each property name with the property name as the map key
574      * and the property value as the value
575      */
576     public static Map<String, String> getPropertyKeyValuesFromDataObject(List<String> propertyNames,
577             List<String> securePropertyNames, Object dataObject) {
578         Map<String, String> propertyKeyValues = new HashMap<String, String>();
579 
580         if (dataObject == null) {
581             return propertyKeyValues;
582         }
583 
584         // iterate through properties and add a map entry for each
585         for (String propertyName : propertyNames) {
586             String propertyValue = ObjectPropertyUtils.getPropertyValueAsText(dataObject, propertyName);
587 
588             if (propertyValue == null) {
589                 propertyValue = StringUtils.EMPTY;
590             }
591 
592             // add the property if it is not secure
593             if (!isSecure(propertyName, securePropertyNames, dataObject.getClass())) {
594                 propertyKeyValues.put(propertyName, propertyValue);
595             }
596         }
597 
598         return propertyKeyValues;
599     }
600 
601     /**
602      * Determines whether a property name should be secured, either based on installed sensitive data patterns, a list
603      * of secure property name patterns, or attributes in the Data Dictionary.
604      *
605      * @param propertyName the property name to check for security
606      * @param dataObjectClass the class of the object containing this property
607      *
608      * @return true if the property needs to be secure, false otherwise
609      */
610     public static boolean isSecure(String propertyName, Class<?> dataObjectClass) {
611         return isSecure(propertyName, Collections.<String>emptyList(), dataObjectClass);
612     }
613 
614     /**
615      * Determines whether a property name should be secured, either based on installed sensitive data patterns, a list
616      * of secure property name patterns, or attributes in the Data Dictionary.
617      *
618      * @param propertyName the property name to check for security
619      * @param securePropertyNames the secure property name patterns to check
620      * @param dataObjectClass the class of the object containing this property
621      *
622      * @return true if the property needs to be secure, false otherwise
623      */
624     public static boolean isSecure(String propertyName, List<String> securePropertyNames, Class<?> dataObjectClass) {
625         if (containsSecurePropertyName(propertyName, securePropertyNames)) {
626             return true;
627         }
628 
629         return KRADServiceLocatorWeb.getDataObjectAuthorizationService()
630                 .attributeValueNeedsToBeEncryptedOnFormsAndLinks(dataObjectClass, propertyName);
631     }
632 
633     /**
634      * Helper method to identify if propertyName contains a secure property name element.
635      * Check handles simple or compound names and ignores partial matches.
636      *
637      * @param propertyName property name as a single term or compound term (i.e. items[0].propertyName)
638      * @param securePropertyNames list of secure property names to match
639      * @return true if any of the secure property names are found in the property name, false otherwise
640      */
641     private static boolean containsSecurePropertyName(String propertyName, List<String> securePropertyNames) {
642         if (securePropertyNames == null) {
643             return false;
644         }
645 
646         for (String securePropertyName : securePropertyNames) {
647             // pattern prefix and suffix used to handle compound names and ignore partial name matches
648             if (Pattern.compile("(?:\\.|^)" + Pattern.quote(securePropertyName) + "(?:\\.|\\[|$)").matcher(propertyName)
649                     .find()) {
650                 return true;
651             }
652         }
653 
654         return false;
655     }
656 
657     /**
658      * Utility method to convert a Map to a Properties object
659      *
660      * @param parameters - map to convert
661      * @return Properties object containing all the map entries
662      */
663     public static Properties convertMapToProperties(Map<String, String> parameters) {
664         Properties properties = new Properties();
665 
666         if (parameters != null) {
667             for (Map.Entry<String, String> parameter : parameters.entrySet()) {
668                 properties.put(parameter.getKey(), parameter.getValue());
669             }
670         }
671 
672         return properties;
673     }
674 
675     /**
676      * Utility method to convert a Request Parameters Map to a Properties object
677      *
678      * <p>
679      * Multiple values for a parameter are joined together with comma delimiter
680      * </p>
681      *
682      * @param requestParameters - map to convert
683      * @return Properties object containing all the map entries
684      */
685     public static Properties convertRequestMapToProperties(Map<String, String[]> requestParameters) {
686         Properties properties = new Properties();
687 
688         if (requestParameters != null) {
689             for (Map.Entry<String, String[]> parameter : requestParameters.entrySet()) {
690                 String[] parameterValue = parameter.getValue();
691                 String parameterValueString = StringUtils.join(parameterValue, ",");
692 
693                 properties.put(parameter.getKey(), parameterValueString);
694             }
695         }
696 
697         return properties;
698     }
699 
700     /**
701      * This method converts request parameters coming in as String to native types in case of Boolean, Number or
702      * java.sql.Date.
703      * For boolean the conversion is performed only if an @Converter annotation is set on the JPA entity field.
704      *
705      * @param dataObjectClass - business object class
706      * @param parameters - map of request parameters with field values as String for the fields in the dataObjectClass
707      * @return Map <String,Object> converted values
708      */
709     public static Map<String, Object> coerceRequestParameterTypes(Class<?> dataObjectClass,
710             Map<String, String> parameters) {
711         Map<String, Object> filteredFieldValues = new HashMap<String, Object>();
712         List<java.lang.reflect.Field> allFields = ObjectPropertyUtils.getAllFields(
713                 new ArrayList<java.lang.reflect.Field>(), dataObjectClass, Object.class);
714 
715         for (String fieldName : parameters.keySet()) {
716             Class<?> propertyType = ObjectPropertyUtils.getPropertyType(dataObjectClass, fieldName);
717 
718             String strValue = parameters.get(fieldName);
719 
720             if (TypeUtils.isBooleanClass(propertyType) && isConvertAnnotationPresent(allFields, fieldName)) {
721                 filteredFieldValues.put(fieldName, Truth.strToBooleanIgnoreCase(strValue));
722             } else if (TypeUtils.isIntegralClass(propertyType) || TypeUtils.isDecimalClass(propertyType)) {
723                 try {
724                     filteredFieldValues.put(fieldName, hydrateAttributeValue(propertyType, strValue));
725                 } catch (Exception nfe) {
726                     GlobalVariables.getMessageMap().putError("parameters[" + fieldName + "]",
727                             RiceKeyConstants.ERROR_NUMBER, strValue);
728                     throw new RuntimeException("Could not parse property value into Number for " + fieldName);
729                 }
730             } else if (TypeUtils.isTemporalClass(propertyType)) {
731                 try {
732                     filteredFieldValues.put(fieldName, CoreApiServiceLocator.getDateTimeService().convertToSqlDate(
733                             strValue));
734                 } catch (ParseException pe) {
735                     GlobalVariables.getMessageMap().putError("parameters[" + fieldName + "]",
736                             RiceKeyConstants.ERROR_DATE_TIME, strValue);
737                     throw new RuntimeException("Could not parse property value into java.sql.Date for " + fieldName);
738                 }
739             }
740 
741             // If value not converted set the value from parameters
742             if (filteredFieldValues.get(fieldName) == null) {
743                 filteredFieldValues.put(fieldName, parameters.get(fieldName));
744             }
745 
746         }
747         return filteredFieldValues;
748 
749     }
750 
751     /**
752      * Checks to see if the specified field from the list of allFields has the @Convert annotation set on it
753      *
754      * @param allFields List of all fields on the entity
755      * @param fieldName Field name to check for @Convert annotation
756      * @return true if annotation is present else false
757      */
758     private static boolean isConvertAnnotationPresent(List<java.lang.reflect.Field> allFields, String fieldName) {
759         //Check if there is a @Convert annotation on the field
760         boolean convertAnnotationFound = false;
761         for (java.lang.reflect.Field f : allFields) {
762             if (f.getName().equalsIgnoreCase(fieldName)) {
763                 if (f.getAnnotation(javax.persistence.Convert.class) != null) {
764                     convertAnnotationFound = true;
765                 }
766                 break;
767             }
768         }
769 
770         return convertAnnotationFound;
771     }
772 
773     /**
774      * Check if data might be sensitive
775      *
776      * <p>
777      * The sensitivity of the data is checked by matching it against the sensitive data patterns that are specified
778      * in the system parameter table.
779      * </p>
780      *
781      * @param fieldValue data to be checked for sensitivity
782      * @return true if the data matches the sensitive data pattern, false otherwise
783      */
784     public static boolean containsSensitiveDataPatternMatch(String fieldValue) {
785         if (StringUtils.isBlank(fieldValue)) {
786             return false;
787         }
788 
789         ParameterService parameterService = CoreFrameworkServiceLocator.getParameterService();
790         Collection<String> sensitiveDataPatterns = parameterService.getParameterValuesAsString(
791                 KRADConstants.KNS_NAMESPACE, ParameterConstants.ALL_COMPONENT,
792                 KRADConstants.SystemGroupParameterNames.SENSITIVE_DATA_PATTERNS);
793 
794         for (String pattern : sensitiveDataPatterns) {
795             if (Pattern.compile(pattern).matcher(fieldValue).find()) {
796                 return true;
797             }
798         }
799 
800         return false;
801     }
802 
803     /**
804      * Strips out common patterns used in cross side scripting.
805      *
806      * @param values string array to strip patterns from
807      * @return cleaned string array
808      */
809     public static String[] stripXSSPatterns(String[] values) {
810         ArrayList<String> strippedValues = new ArrayList<String>();
811 
812         for (String value : values) {
813             strippedValues.add(stripXSSPatterns(value));
814         }
815 
816         return strippedValues.toArray(new String[]{});
817     }
818 
819     /**
820      * Strips out common patterns used in cross side scripting.
821      *
822      * @param value string to strip patterns from
823      * @return cleaned string
824      */
825     public static String stripXSSPatterns(String value) {
826         if (value == null) {
827             return null;
828         }
829 
830         // Avoid null characters
831         value = value.replaceAll("", "");
832 
833         // Avoid anything between script tags
834         Pattern scriptPattern = Pattern.compile("<script>(.*?)</script>", Pattern.CASE_INSENSITIVE);
835         value = scriptPattern.matcher(value).replaceAll("");
836 
837         // Avoid anything in a src='...' type of expression
838         scriptPattern = Pattern.compile("src[\r\n]*=[\r\n]*\\\'(.*?)\\\'",
839                 Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
840         value = scriptPattern.matcher(value).replaceAll("");
841 
842         scriptPattern = Pattern.compile("src[\r\n]*=[\r\n]*\\\"(.*?)\\\"",
843                 Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
844         value = scriptPattern.matcher(value).replaceAll("");
845 
846         // Remove any lonesome </script> tag
847         scriptPattern = Pattern.compile("</script>", Pattern.CASE_INSENSITIVE);
848         value = scriptPattern.matcher(value).replaceAll("");
849 
850         // Remove any lonesome <script ...> tag
851         scriptPattern = Pattern.compile("<script(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
852         value = scriptPattern.matcher(value).replaceAll("");
853 
854         // Avoid eval(...) expressions
855         scriptPattern = Pattern.compile("eval\\((.*?)\\)",
856                 Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
857         value = scriptPattern.matcher(value).replaceAll("");
858 
859         // Avoid expression(...) expressions
860         scriptPattern = Pattern.compile("expression\\((.*?)\\)",
861                 Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
862         value = scriptPattern.matcher(value).replaceAll("");
863 
864         // Avoid javascript:... expressions
865         scriptPattern = Pattern.compile("javascript:", Pattern.CASE_INSENSITIVE);
866         value = scriptPattern.matcher(value).replaceAll("");
867 
868         // Avoid vbscript:... expressions
869         scriptPattern = Pattern.compile("vbscript:", Pattern.CASE_INSENSITIVE);
870         value = scriptPattern.matcher(value).replaceAll("");
871 
872         // Avoid onload= expressions
873         scriptPattern = Pattern.compile("onload(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
874         value = scriptPattern.matcher(value).replaceAll("");
875 
876         return value;
877     }
878 
879     /**
880      * Gets the UserSession object from the HttpServletRequest object's
881      * associated session.
882      *
883      * <p>
884      * In some cases (different threads) the UserSession cannot be retrieved
885      * from GlobalVariables but can still be accessed via the session object
886      * </p>
887      *
888      * @param request servlet request
889      * @return user session found in the request's servlet session
890      */
891     public static final UserSession getUserSessionFromRequest(HttpServletRequest request) {
892         return (UserSession) request.getSession().getAttribute(KRADConstants.USER_SESSION_KEY);
893     }
894 
895     /**
896      * Check if current deployment is the production environment
897      *
898      * @return true if the deploy environment is production, false otherwise
899      */
900     public static boolean isProductionEnvironment() {
901         return CoreApiServiceLocator.getKualiConfigurationService().getPropertyValueAsString(
902                 KRADConstants.PROD_ENVIRONMENT_CODE_KEY).equalsIgnoreCase(
903                 CoreApiServiceLocator.getKualiConfigurationService().getPropertyValueAsString(
904                         KRADConstants.ENVIRONMENT_KEY));
905     }
906 
907     /**
908      * Gets the message associated with ErrorMessage object passed in, using message service.
909      * The prefix and suffix will be appended to the retrieved message if processPrefixSuffix is true and if those
910      * settings are set on the ErrorMessage passed in.
911      *
912      * @param errorMessage the ErrorMessage object containing the message key(s)
913      * @param processPrefixSuffix if true appends the prefix and suffix to the message if they exist on ErrorMessage
914      * @return the converted/retrieved message
915      */
916     public static String getMessageText(ErrorMessage errorMessage, boolean processPrefixSuffix) {
917         String message = "";
918         if (errorMessage != null && errorMessage.getErrorKey() != null) {
919             MessageService messageService = KRADServiceLocatorWeb.getMessageService();
920 
921             // find message by key
922             message = messageService.getMessageText(errorMessage.getNamespaceCode(), errorMessage.getComponentCode(),
923                     errorMessage.getErrorKey());
924             if (message == null) {
925                 message = "Intended message with key: " + errorMessage.getErrorKey() + " not found.";
926             }
927 
928             if (errorMessage.getMessageParameters() != null && StringUtils.isNotBlank(message)) {
929                 message = message.replace("'", "''");
930                 message = MessageFormat.format(message, (Object[]) errorMessage.getMessageParameters());
931             }
932 
933             // add prefix
934             if (StringUtils.isNotBlank(errorMessage.getMessagePrefixKey()) && processPrefixSuffix) {
935                 String prefix = messageService.getMessageText(errorMessage.getNamespaceCode(),
936                         errorMessage.getComponentCode(), errorMessage.getMessagePrefixKey());
937 
938                 if (errorMessage.getMessagePrefixParameters() != null && StringUtils.isNotBlank(prefix)) {
939                     prefix = prefix.replace("'", "''");
940                     prefix = MessageFormat.format(prefix, (Object[]) errorMessage.getMessagePrefixParameters());
941                 }
942 
943                 if (StringUtils.isNotBlank(prefix)) {
944                     message = prefix + " " + message;
945                 }
946             }
947 
948             // add suffix
949             if (StringUtils.isNotBlank(errorMessage.getMessageSuffixKey()) && processPrefixSuffix) {
950                 String suffix = messageService.getMessageText(errorMessage.getNamespaceCode(),
951                         errorMessage.getComponentCode(), errorMessage.getMessageSuffixKey());
952 
953                 if (errorMessage.getMessageSuffixParameters() != null && StringUtils.isNotBlank(suffix)) {
954                     suffix = suffix.replace("'", "''");
955                     suffix = MessageFormat.format(suffix, (Object[]) errorMessage.getMessageSuffixParameters());
956                 }
957 
958                 if (StringUtils.isNotBlank(suffix)) {
959                     message = message + " " + suffix;
960                 }
961             }
962         }
963 
964         return message;
965     }
966 
967     /**
968      * Logs the error messages if any in the message map
969      */
970     public static void logErrors() {
971         if (!GlobalVariables.getMessageMap().hasErrors()) {
972             return;
973         }
974 
975         for (Iterator<Map.Entry<String, List<ErrorMessage>>> i =
976                      GlobalVariables.getMessageMap().getAllPropertiesAndErrors().iterator(); i.hasNext(); ) {
977             Map.Entry<String, List<ErrorMessage>> e = i.next();
978 
979             StringBuffer logMessage = buildMessage(e);
980             LOG.error(logMessage);
981         }
982     }
983 
984     /**
985      * Builds the message for a given entry in the messageMap. The entry could have multiple messages for a given key.
986      * The messages are appended separated by a ;
987      *
988      * @param e Map entry of property and errors for that property
989      * @return logMessage
990      */
991     private static StringBuffer buildMessage(Map.Entry<String, List<ErrorMessage>> e) {
992         StringBuffer logMessage = new StringBuffer();
993         logMessage.append("[" + e.getKey() + "] ");
994         boolean first = true;
995 
996         List<ErrorMessage> errorList = e.getValue();
997         for (Iterator<ErrorMessage> j = errorList.iterator(); j.hasNext(); ) {
998             ErrorMessage em = j.next();
999 
1000             // if its the first message for the key
1001             if (first) {
1002                 first = false;
1003             } else {
1004                 logMessage.append(";");
1005             }
1006             logMessage.append(em);
1007         }
1008         return logMessage;
1009     }
1010 
1011     /**
1012      * Generate the request parameter portion of the url based on the map of key value pairs passed in
1013      *
1014      * @param requestParameters the request parameters to use in the string
1015      * @return a request parameter string starting with "?" with "&" separators, or blank if the mapped passed in is
1016      * blank
1017      */
1018     public static String getRequestStringFromMap(Map<String, String> requestParameters) {
1019         String requestString = "";
1020 
1021         if (requestParameters.isEmpty()) {
1022             return requestString;
1023         }
1024 
1025         URLCodec urlCodec = new URLCodec(KRADConstants.DEFAULT_ENCODING);
1026 
1027         for (String key : requestParameters.keySet()) {
1028             String value = null;
1029             try {
1030                 value = urlCodec.encode(requestParameters.get(key));
1031             } catch (EncoderException e) {
1032                 throw new RuntimeException("Unable to encode parameter name or value: " + key + "=" + value, e);
1033             }
1034 
1035             if (StringUtils.isNotBlank(requestString)) {
1036                 requestString = requestString + "&";
1037             }
1038 
1039             requestString = requestString + key + "=" + value;
1040         }
1041 
1042         return "?" + requestString;
1043     }
1044 
1045     /**
1046      * Adds the header and content of an attachment to the response.
1047      *
1048      * @param response HttpServletResponse instance
1049      * @param contentType the content type of the attachment
1050      * @param inputStream the content of the attachment
1051      * @param fileName the file name of the attachment
1052      * @param fileSize the size of the attachment
1053      * @throws IOException if attachment to the results fails due to an IO error
1054      */
1055     public static void addAttachmentToResponse(HttpServletResponse response, InputStream inputStream,
1056             String contentType, String fileName, long fileSize) throws IOException {
1057 
1058         // If there are quotes in the name, we should replace them to avoid issues.
1059         // The filename will be wrapped with quotes below when it is set in the header
1060         String updateFileName;
1061         if (fileName.contains("\"")) {
1062             updateFileName = fileName.replaceAll("\"", "");
1063         } else {
1064             updateFileName = fileName;
1065         }
1066 
1067         // set response
1068         response.setContentType(contentType);
1069         response.setContentLength(org.springframework.util.NumberUtils.convertNumberToTargetClass(fileSize,
1070                 Integer.class));
1071         response.setHeader("Content-disposition", "attachment; filename=\"" + updateFileName + "\"");
1072         response.setHeader("Expires", "0");
1073         response.setHeader("Cache-Control", "must-revalidate, post-check=0, pre-check=0");
1074         response.setHeader("Pragma", "public");
1075 
1076         // Copy the input stream to the response
1077         if (inputStream != null) {
1078             FileCopyUtils.copy(inputStream, response.getOutputStream());
1079         }
1080     }
1081 
1082     /**
1083      * Helper method for building a URL that will invoke the given controller and render the given
1084      * KRAD view
1085      *
1086      * @param baseUrl base url (domain, port)
1087      * @param controllerMapping mapping for the controller that should be invoked
1088      * @param viewId id for the view that should be rendered
1089      * @return url for invoking the view
1090      */
1091     public static String buildViewUrl(String baseUrl, String controllerMapping, String viewId) {
1092         Assert.hasLength(baseUrl, "base url is null or empty");
1093         Assert.hasLength(controllerMapping, "controller mapping is null or empty");
1094         Assert.hasLength(viewId, "view id is null or empty");
1095 
1096         StringBuffer url = new StringBuffer();
1097 
1098         url.append(baseUrl);
1099 
1100         if (!baseUrl.endsWith("/")) {
1101             url.append("/");
1102         }
1103 
1104         url.append(controllerMapping);
1105 
1106         url.append("?");
1107         url.append(UifParameters.VIEW_ID);
1108         url.append("=");
1109         url.append(viewId);
1110 
1111         return url.toString();
1112     }
1113 
1114     /**
1115      * Removes parameters from the given properties object that are request specific (useful when manupulating the
1116      * current URL to invoke something else)
1117      *
1118      * @param requestParameters properties instance containing the parameters to clean
1119      */
1120     public static void cleanRequestParameters(Properties requestParameters) {
1121         requestParameters.remove(UifParameters.SESSION_ID);
1122         requestParameters.remove(UifParameters.AJAX_REQUEST);
1123         requestParameters.remove(UifParameters.AJAX_RETURN_TYPE);
1124         requestParameters.remove(UifParameters.FORM_KEY);
1125         requestParameters.remove(UifParameters.JUMP_TO_ID);
1126         requestParameters.remove(UifParameters.FOCUS_ID);
1127     }
1128 
1129     /**
1130      * Get the full url for a request (requestURL + queryString)
1131      *
1132      * @param request the request
1133      * @return the fullUrl
1134      */
1135     public static String getFullURL(HttpServletRequest request) {
1136         if (request == null) {
1137             return null;
1138         }
1139 
1140         StringBuffer requestURL = request.getRequestURL();
1141         String queryString = request.getQueryString();
1142 
1143         if (queryString == null) {
1144             return requestURL.toString();
1145         } else {
1146             return requestURL.append('?').append(queryString).toString();
1147         }
1148     }
1149 
1150     /**
1151      * Determines whether two URLs point at the same domain.
1152      *
1153      * @param firstDomain first URL string to compare
1154      * @param secondDomain second URL string to compare
1155      * @return true if the domains are different, false otherwise
1156      */
1157     public static boolean areDifferentDomains(String firstDomain, String secondDomain) {
1158         try {
1159             URL urlOne = new URL(firstDomain.toLowerCase());
1160             URL urlTwo = new URL(secondDomain.toLowerCase());
1161 
1162             if (urlOne.getHost().equals(urlTwo.getHost())) {
1163                 LOG.debug("Hosts "
1164                         + urlOne.getHost()
1165                         + " of domains "
1166                         + firstDomain
1167                         + " and "
1168                         + secondDomain
1169                         + " were determined to be equal");
1170 
1171                 return false;
1172             } else {
1173                 LOG.debug("Hosts "
1174                         + urlOne.getHost()
1175                         + " of domains "
1176                         + firstDomain
1177                         + " and "
1178                         + secondDomain
1179                         + " are not equal");
1180 
1181                 return true;
1182             }
1183         } catch (MalformedURLException mue) {
1184             LOG.error("Unable to successfully compare domains " + firstDomain + " and " + secondDomain);
1185         }
1186 
1187         return true;
1188     }
1189 
1190     /**
1191      * Attempts to generate a unique view title by combining the View's headerText with the title attribute for the
1192      * dataObjectClass found through the DataObjectMetaDataService.  If the title attribute cannot be found, just the
1193      * headerText is returned.
1194      *
1195      * @param form the form
1196      * @param view the view
1197      * @return the headerText with the title attribute in parenthesis or just the headerText if it title attribute
1198      * cannot be determined
1199      */
1200     public static String generateUniqueViewTitle(UifFormBase form, View view) {
1201         String title = view.getHeader().getHeaderText();
1202 
1203         String viewLabelPropertyName = "";
1204 
1205         Class<?> dataObjectClass;
1206         if (StringUtils.isNotBlank(view.getDefaultBindingObjectPath())) {
1207             dataObjectClass = ObjectPropertyUtils.getPropertyType(form, view.getDefaultBindingObjectPath());
1208         } else {
1209             dataObjectClass = view.getFormClass();
1210         }
1211 
1212         if (dataObjectClass != null) {
1213             viewLabelPropertyName = KRADServiceLocatorWeb.getLegacyDataAdapter().getTitleAttribute(dataObjectClass);
1214         }
1215 
1216         String viewLabelPropertyPath = "";
1217         if (StringUtils.isNotBlank(viewLabelPropertyName)) {
1218             // adjust binding prefix
1219             if (!viewLabelPropertyName.startsWith(UifConstants.NO_BIND_ADJUST_PREFIX)) {
1220                 if (StringUtils.isNotBlank(view.getDefaultBindingObjectPath())) {
1221                     viewLabelPropertyPath = view.getDefaultBindingObjectPath() + "." + viewLabelPropertyName;
1222                 }
1223             } else {
1224                 viewLabelPropertyPath = StringUtils.removeStart(viewLabelPropertyName,
1225                         UifConstants.NO_BIND_ADJUST_PREFIX);
1226             }
1227         } else {
1228             // attempt to get title attribute
1229             if (StringUtils.isNotBlank(view.getDefaultBindingObjectPath())) {
1230                 dataObjectClass = ViewModelUtils.getObjectClassForMetadata(view, form,
1231                         view.getDefaultBindingObjectPath());
1232             } else {
1233                 dataObjectClass = view.getFormClass();
1234             }
1235 
1236             if (dataObjectClass != null) {
1237                 String titleAttribute = KRADServiceLocatorWeb.getLegacyDataAdapter().getTitleAttribute(dataObjectClass);
1238                 if (StringUtils.isNotBlank(titleAttribute)) {
1239                     viewLabelPropertyPath = view.getDefaultBindingObjectPath() + "." + titleAttribute;
1240                 }
1241             }
1242         }
1243 
1244         Object viewLabelPropertyValue = null;
1245         if (StringUtils.isNotBlank(viewLabelPropertyPath) && ObjectPropertyUtils.isReadableProperty(form,
1246                 viewLabelPropertyPath)) {
1247             viewLabelPropertyValue = ObjectPropertyUtils.getPropertyValueAsText(form, viewLabelPropertyPath);
1248         }
1249 
1250         if (viewLabelPropertyValue != null && StringUtils.isNotBlank(viewLabelPropertyValue.toString()) && StringUtils
1251                 .isNotBlank(title)) {
1252             return title + " (" + viewLabelPropertyValue.toString() + ")";
1253         } else {
1254             return title;
1255         }
1256     }
1257 
1258     /**
1259      * Helper method for building title text for an element and a map of key/value pairs,
1260      *
1261      * <p>
1262      * Each key of the key value map is assumed to be an attribute for the given element class. The label is then
1263      * retrieved for the attribute from the data dictionary and used in the title (instead of the key)
1264      * </p>
1265      *
1266      * @param prependText text to prepend to the title
1267      * @param element element class the title is being generated for, used as the parent for getting the key labels
1268      * @param keyValueMap map of key value pairs to add to the title text
1269      * @return title string
1270      */
1271     public static String buildAttributeTitleString(String prependText, Class<?> element,
1272             Map<String, String> keyValueMap) {
1273         StringBuffer titleText = new StringBuffer(prependText);
1274 
1275         for (String key : keyValueMap.keySet()) {
1276             String fieldVal = keyValueMap.get(key).toString();
1277 
1278             titleText.append(" "
1279                     + KRADServiceLocatorWeb.getDataDictionaryService().getAttributeLabel(element, key)
1280                     + "="
1281                     + fieldVal.toString());
1282         }
1283 
1284         return titleText.toString();
1285     }
1286 
1287     /**
1288      * Attempts to extract a string value out of the field passed in, varies depending on field type
1289      *
1290      * <p>If the field is a dataField, it will use its propertyName to retrieve a value, otherwise it will try to
1291      * retrieve textual content out of various component types.  If the field is a FieldGroup, only the first
1292      * component's determined value will be used.  This function is used for sorting.</p>
1293      *
1294      * @param model the current model
1295      * @param field the field to get a value from
1296      * @return the field's String value, false if it cant be determined
1297      */
1298     public static String getSimpleFieldValue(Object model, Field field) {
1299         if (field == null) {
1300             return null;
1301         }
1302 
1303         String value = null;
1304         // check for what type of field this is
1305         if (field instanceof DataField) {
1306             String propertyPath = ((DataField) field).getBindingInfo().getBindingPath();
1307             Object valueObject = null;
1308 
1309             if (field.isHidden()) {
1310                 return "";
1311             }
1312 
1313             // check if readable
1314             if (ObjectPropertyUtils.isReadableProperty(model, propertyPath)) {
1315                 valueObject = ObjectPropertyUtils.getPropertyValueAsText(model, propertyPath);
1316             }
1317 
1318             // use object's string value
1319             if (valueObject != null && !((DataField) field).isApplyMask()) {
1320                 value = valueObject.toString();
1321             } else if (valueObject != null && ((DataField) field).isApplyMask()) {
1322                 value = ((DataField) field).getMaskFormatter().maskValue(valueObject);
1323             }
1324         } else if (field instanceof ActionField) {
1325             value = ((ActionField) field).getActionLabel();
1326 
1327             // use image alt text if any
1328             if (StringUtils.isBlank(value) && ((ActionField) field).getActionImage() != null) {
1329                 value = ((ActionField) field).getActionImage().getAltText();
1330             }
1331         } else if (field instanceof LinkField) {
1332             value = ((LinkField) field).getLinkText();
1333         } else if (field instanceof ImageField) {
1334             value = ((ImageField) field).getAltText();
1335         } else if (field instanceof MessageField && ((MessageField) field).getMessage() != null) {
1336             value = ((MessageField) field).getMessage().getMessageText();
1337         } else if (field instanceof SpaceField) {
1338             value = "";
1339         } else if (field instanceof FieldGroup
1340                 && ((FieldGroup) field).getGroup() != null
1341                 && ((FieldGroup) field).getGroup().getItems() != null
1342                 && !((FieldGroup) field).getGroup().getItems().isEmpty()) {
1343             // using first components type for assumed value
1344             Component firstComponent = ((FieldGroup) field).getGroup().getItems().get(0);
1345 
1346             // check first component type to extract value
1347             if (firstComponent != null && firstComponent instanceof Field) {
1348                 value = getSimpleFieldValue(model, (Field) firstComponent);
1349             } else if (firstComponent instanceof Action && StringUtils.isNotBlank(
1350                     ((Action) firstComponent).getActionLabel())) {
1351                 value = ((Action) firstComponent).getActionLabel();
1352             } else if (firstComponent instanceof Action && ((Action) firstComponent).getActionImage() != null) {
1353                 value = ((Action) firstComponent).getActionImage().getAltText();
1354             } else if (firstComponent instanceof Link) {
1355                 value = ((Link) firstComponent).getLinkText();
1356             } else if (firstComponent instanceof Image) {
1357                 value = ((Image) firstComponent).getAltText();
1358             } else if (firstComponent instanceof org.kuali.rice.krad.uif.element.Message) {
1359                 value = ((org.kuali.rice.krad.uif.element.Message) firstComponent).getMessageText();
1360             } else {
1361                 value = null;
1362             }
1363         }
1364 
1365         return value;
1366     }
1367 
1368     /**
1369      * Helper method to change common characters into HTML attribute safe characters
1370      *
1371      * @param message the string to convert
1372      * @return the converted string with quotes, sing quotes, and slash replaced
1373      */
1374     public static String convertToHTMLAttributeSafeString(String message) {
1375         if (StringUtils.isBlank(message)) {
1376             return message;
1377         }
1378 
1379         if (message.contains("\"")) {
1380             message = message.replace("\"", "&quot;");
1381         }
1382         if (message.contains("'")) {
1383             message = message.replace("'", "&#39;");
1384         }
1385         if (message.contains("\\")) {
1386             message = message.replace("\\", "&#92;");
1387         }
1388 
1389         return message;
1390     }
1391 
1392     /**
1393      * Get the rowCss for the line specified, by evaluating the conditionalRowCssClasses map for that row
1394      *
1395      * @param conditionalRowCssClasses the conditionalRowCssClass map, where key is the condition and value is
1396      * the class(es)
1397      * @param lineIndex the line/row index
1398      * @param isOdd true if the row is considered odd
1399      * @param lineContext the lineContext for expressions, pass null if not applicable
1400      * @param expressionEvaluator the expressionEvaluator, pass null if not applicable
1401      * @return row csss class String for the class attribute of this row
1402      */
1403     public static String generateRowCssClassString(Map<String, String> conditionalRowCssClasses, int lineIndex,
1404             boolean isOdd, Map<String, Object> lineContext, ExpressionEvaluator expressionEvaluator) {
1405         String rowCss = "";
1406         if (conditionalRowCssClasses == null || conditionalRowCssClasses.isEmpty()) {
1407             return rowCss;
1408         }
1409 
1410         for (String cssRule : conditionalRowCssClasses.keySet()) {
1411             if (cssRule.startsWith(UifConstants.EL_PLACEHOLDER_PREFIX) && lineContext != null &&
1412                     expressionEvaluator != null) {
1413                 String outcome = expressionEvaluator.evaluateExpressionTemplate(lineContext, cssRule);
1414                 if (outcome != null && Boolean.parseBoolean(outcome)) {
1415                     rowCss = rowCss + " " + conditionalRowCssClasses.get(cssRule);
1416                 }
1417             } else if (cssRule.equals(UifConstants.RowSelection.ALL)) {
1418                 rowCss = rowCss + " " + conditionalRowCssClasses.get(cssRule);
1419             } else if (cssRule.equals(UifConstants.RowSelection.EVEN) && !isOdd) {
1420                 rowCss = rowCss + " " + conditionalRowCssClasses.get(cssRule);
1421             } else if (cssRule.equals(UifConstants.RowSelection.ODD) && isOdd) {
1422                 rowCss = rowCss + " " + conditionalRowCssClasses.get(cssRule);
1423             } else if (StringUtils.isNumeric(cssRule) && (lineIndex + 1) == Integer.parseInt(cssRule)) {
1424                 rowCss = rowCss + " " + conditionalRowCssClasses.get(cssRule);
1425             }
1426         }
1427 
1428         rowCss = StringUtils.removeStart(rowCss, " ");
1429 
1430         return rowCss;
1431     }
1432 
1433     /**
1434      * LegacyCase - This method simply uses PojoPropertyUtilsBean logic to get the Class of a Class property.
1435      * This method does not have any of the logic needed to obtain the Class of an element of a Collection specified in
1436      * the DataDictionary.
1437      *
1438      * @param object An instance of the Class of which we're trying to get the property Class.
1439      * @param propertyName The name of the property.
1440      * @return property type
1441      * @throws IllegalAccessException
1442      * @throws NoSuchMethodException
1443      * @throws InvocationTargetException
1444      */
1445     static public Class easyGetPropertyType(Object object,
1446             String propertyName) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
1447         if (LegacyUtils.useLegacyForObject(object)) {
1448             return PropertyUtils.getPropertyType(object, propertyName);
1449         }
1450         return KradDataServiceLocator.getDataObjectService().wrap(object).getPropertyType(propertyName);
1451     }
1452 
1453     /**
1454      * Sets the property of an object with the given value. Converts using the formatter of the type for the property.
1455      * Note: propertyType does not need passed, is found by util method.
1456      *
1457      * @param bo business object
1458      * @param propertyName property name
1459      * @param propertyValue propery value
1460      * @throws IllegalAccessException
1461      * @throws InvocationTargetException
1462      * @throws NoSuchMethodException
1463      */
1464     public static void setObjectProperty(Object bo, String propertyName,
1465             Object propertyValue) throws FormatException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
1466         Class propertyType = easyGetPropertyType(bo, propertyName);
1467         setObjectProperty(bo, propertyName, propertyType, propertyValue);
1468 
1469     }
1470 
1471     /**
1472      * Sets the property of an object with the given value. Converts using the formatter of the given type if one is
1473      * found.
1474      *
1475      * @param bo
1476      * @param propertyName
1477      * @param propertyType
1478      * @param propertyValue
1479      * @throws NoSuchMethodException
1480      * @throws InvocationTargetException
1481      * @throws IllegalAccessException
1482      */
1483     public static void setObjectProperty(Object bo, String propertyName, Class propertyType,
1484             Object propertyValue) throws FormatException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
1485         KRADServiceLocatorWeb.getLegacyDataAdapter().setObjectProperty(bo, propertyName, propertyType, propertyValue);
1486     }
1487 
1488     /**
1489      * Removes all query characters from a string.
1490      *
1491      * @param string
1492      * @return Cleaned string
1493      */
1494     public static String clean(String string) {
1495         for (SearchOperator op : SearchOperator.QUERY_CHARACTERS) {
1496             string = StringUtils.replace(string, op.op(), KRADConstants.EMPTY_STRING);
1497         }
1498         return string;
1499     }
1500 
1501     /**
1502      * This method is a confirms if object is null, and unproxies if necessary to determine this
1503      *
1504      * @param object - any object, proxied or not, materialized or not
1505      * @return true if the object (or underlying materialized object) is null, false otherwise
1506      */
1507     public static boolean isNull(Object object) {
1508         if (object == null) {
1509             return true;
1510         }
1511         return KRADServiceLocatorWeb.getLegacyDataAdapter().isNull(object);
1512     }
1513 
1514     /**
1515      * This method is a confirms if object is not null, and unproxies if necessary to determine this
1516      *
1517      * @param object - any object, proxied or not, materialized or not
1518      * @return true if the object (or underlying materialized object) is not null, true if its null
1519      */
1520     public static boolean isNotNull(Object object) {
1521         return !isNull(object);
1522     }
1523 
1524     /**
1525      * Attempts to find the Class for the given potentially proxied object
1526      *
1527      * @param object the potentially proxied object to find the Class of
1528      * @return the best Class which could be found for the given object
1529      */
1530     public static Class materializeClassForProxiedObject(Object object) {
1531         return KRADServiceLocatorWeb.getLegacyDataAdapter().materializeClassForProxiedObject(object);
1532     }
1533 
1534     /**
1535      * This method runs the KRADUtils.isNotNull() method for each item in a list of BOs. KRADUtils.isNotNull() will
1536      * materialize
1537      * the objects if they are currently OJB proxies.
1538      *
1539      * @param possiblyProxiedObjects - a Collection of objects that may be proxies
1540      */
1541     public static void materializeObjects(Collection possiblyProxiedObjects) {
1542         for (Iterator i = possiblyProxiedObjects.iterator(); i.hasNext(); ) {
1543             KRADUtils.isNotNull(i.next());
1544         }
1545     }
1546 
1547     /**
1548      * Returns the prefix of a nested attribute name, or the empty string if the attribute name is not nested.
1549      *
1550      * @param attributeName
1551      * @return everything BEFORE the last "." character in attributeName
1552      */
1553     public static String getNestedAttributePrefix(String attributeName) {
1554         int lastIndex = PropertyAccessorUtils.getLastNestedPropertySeparatorIndex(attributeName);
1555 
1556         return lastIndex != -1 ? StringUtils.substring(attributeName, 0, lastIndex) : StringUtils.EMPTY;
1557     }
1558 
1559     /**
1560      * Returns the primitive part of an attribute name string.
1561      *
1562      * @param attributeName
1563      * @return everything AFTER the last "." character in attributeName
1564      */
1565     public static String getNestedAttributePrimitive(String attributeName) {
1566         int lastIndex = PropertyAccessorUtils.getLastNestedPropertySeparatorIndex(attributeName);
1567 
1568         return lastIndex != -1 ? StringUtils.substring(attributeName, lastIndex + 1) : attributeName;
1569     }
1570 
1571     /**
1572      * This method safely extracts either simple values OR nested values. For example, if the bo is SubAccount, and the
1573      * fieldName is
1574      * a21SubAccount.subAccountTypeCode, this thing makes sure it gets the value off the very end attribute, no matter
1575      * how deeply
1576      * nested it is. The code would be slightly simpler if this was done recursively, but this is safer, and consumes a
1577      * constant
1578      * amount of memory, no matter how deeply nested it goes.
1579      *
1580      * @param bo
1581      * @param fieldName
1582      * @return The field value if it exists. If it doesnt, and the name is invalid, and
1583      */
1584     public static Object getNestedValue(Object bo, String fieldName) {
1585         return KRADServiceLocatorWeb.getLegacyDataAdapter().getNestedValue(bo, fieldName);
1586     }
1587 
1588     /**
1589      * This method safely creates a object from a class
1590      * Convenience method to create new object and throw a runtime exception if it cannot
1591      * If the class is an {@link org.kuali.rice.krad.bo.ExternalizableBusinessObject}, this method will determine the
1592      * interface for the EBO and
1593      * query the
1594      * appropriate module service to create a new instance.
1595      *
1596      * @param clazz
1597      * @return a newInstance() of clazz
1598      */
1599     public static <T> T createNewObjectFromClass(Class<T> clazz) {
1600         if (clazz == null) {
1601             throw new RuntimeException("BO class was passed in as null");
1602         }
1603         return (T) KRADServiceLocatorWeb.getLegacyDataAdapter().createNewObjectFromClass(clazz);
1604     }
1605 
1606     private static KualiModuleService getKualiModuleService() {
1607         if (kualiModuleService == null) {
1608             kualiModuleService = KRADServiceLocatorWeb.getKualiModuleService();
1609         }
1610         return kualiModuleService;
1611     }
1612 
1613     /**
1614      * Updates the properties of the given component instance with the value found from the
1615      * corresponding map of client state (if found)
1616      *
1617      * @param component component instance to update
1618      * @param clientSideState map of state to sync with
1619      */
1620     public static void syncClientSideStateForComponent(Component component, Map<String, Object> clientSideState) {
1621         // find the map of state that was sent for component (if any)
1622         Map<String, Object> componentState = null;
1623         if (component instanceof View) {
1624             componentState = clientSideState;
1625         } else {
1626             if (clientSideState.containsKey(component.getId())) {
1627                 componentState = (Map<String, Object>) clientSideState.get(component.getId());
1628             }
1629         }
1630 
1631         // if state was sent, match with fields on the component that are annotated to have client state
1632         if ((componentState != null) && (!componentState.isEmpty())) {
1633             Map<String, Annotation> annotatedFields = CopyUtils.getFieldsWithAnnotation(component.getClass(),
1634                     ClientSideState.class);
1635 
1636             for (Map.Entry<String, Annotation> annotatedField : annotatedFields.entrySet()) {
1637                 ClientSideState clientSideStateAnnot = (ClientSideState) annotatedField.getValue();
1638 
1639                 String variableName = clientSideStateAnnot.variableName();
1640                 if (StringUtils.isBlank(variableName)) {
1641                     variableName = annotatedField.getKey();
1642                 }
1643 
1644                 if (componentState.containsKey(variableName)) {
1645                     Object value = componentState.get(variableName);
1646                     ObjectPropertyUtils.setPropertyValue(component, annotatedField.getKey(), value);
1647                 }
1648             }
1649         }
1650     }
1651 
1652 }