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.krad.util;
17  
18  import org.apache.commons.beanutils.PropertyUtils;
19  import org.apache.commons.codec.EncoderException;
20  import org.apache.commons.codec.net.URLCodec;
21  import org.apache.commons.lang.StringUtils;
22  import org.apache.log4j.Logger;
23  import org.kuali.rice.core.api.CoreApiServiceLocator;
24  import org.kuali.rice.core.api.encryption.EncryptionService;
25  import org.kuali.rice.core.api.search.SearchOperator;
26  import org.kuali.rice.core.api.util.RiceKeyConstants;
27  import org.kuali.rice.core.api.util.Truth;
28  import org.kuali.rice.core.api.util.type.KualiDecimal;
29  import org.kuali.rice.core.api.util.type.TypeUtils;
30  import org.kuali.rice.core.web.format.BooleanFormatter;
31  import org.kuali.rice.core.web.format.FormatException;
32  import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
33  import org.kuali.rice.coreservice.framework.parameter.ParameterConstants;
34  import org.kuali.rice.coreservice.framework.parameter.ParameterService;
35  import org.kuali.rice.krad.UserSession;
36  import org.kuali.rice.krad.data.KradDataServiceLocator;
37  import org.kuali.rice.krad.messages.MessageService;
38  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
39  import org.kuali.rice.krad.service.KualiModuleService;
40  import org.kuali.rice.krad.service.ModuleService;
41  import org.kuali.rice.krad.uif.UifConstants;
42  import org.kuali.rice.krad.uif.UifParameters;
43  import org.kuali.rice.krad.uif.component.ClientSideState;
44  import org.kuali.rice.krad.uif.component.Component;
45  import org.kuali.rice.krad.uif.element.Action;
46  import org.kuali.rice.krad.uif.element.Image;
47  import org.kuali.rice.krad.uif.element.Link;
48  import org.kuali.rice.krad.uif.field.ActionField;
49  import org.kuali.rice.krad.uif.field.DataField;
50  import org.kuali.rice.krad.uif.field.Field;
51  import org.kuali.rice.krad.uif.field.FieldGroup;
52  import org.kuali.rice.krad.uif.field.ImageField;
53  import org.kuali.rice.krad.uif.field.LinkField;
54  import org.kuali.rice.krad.uif.field.MessageField;
55  import org.kuali.rice.krad.uif.field.SpaceField;
56  import org.kuali.rice.krad.uif.util.CloneUtils;
57  import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
58  import org.kuali.rice.krad.uif.util.ViewModelUtils;
59  import org.kuali.rice.krad.uif.view.ExpressionEvaluator;
60  import org.kuali.rice.krad.uif.view.View;
61  import org.kuali.rice.krad.web.form.UifFormBase;
62  import org.springframework.beans.PropertyAccessorUtils;
63  import org.springframework.util.Assert;
64  import org.springframework.util.FileCopyUtils;
65  
66  import javax.servlet.ServletRequest;
67  import javax.servlet.http.HttpServletRequest;
68  import javax.servlet.http.HttpServletResponse;
69  import java.io.IOException;
70  import java.io.InputStream;
71  import java.lang.annotation.Annotation;
72  import java.lang.reflect.Constructor;
73  import java.lang.reflect.InvocationTargetException;
74  import java.lang.reflect.Method;
75  import java.math.BigDecimal;
76  import java.net.MalformedURLException;
77  import java.net.URL;
78  import java.security.GeneralSecurityException;
79  import java.text.MessageFormat;
80  import java.text.NumberFormat;
81  import java.text.ParseException;
82  import java.util.ArrayList;
83  import java.util.Arrays;
84  import java.util.Collection;
85  import java.util.Collections;
86  import java.util.HashMap;
87  import java.util.Iterator;
88  import java.util.List;
89  import java.util.Map;
90  import java.util.Properties;
91  import java.util.regex.Pattern;
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 = StringUtils.split(fieldConversionStr, ":");
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 = StringUtils.split(parameter, ":");
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 if this element was encrypted, if it was decrypt it
522                 if (KRADServiceLocatorWeb.getDataObjectAuthorizationService()
523                         .attributeValueNeedsToBeEncryptedOnFormsAndLinks(parentObjectClass, keyPropertyName)) {
524                     try {
525                         keyValue = StringUtils.removeEnd(keyValue, EncryptionService.ENCRYPTION_POST_PREFIX);
526                         keyValue = CoreApiServiceLocator.getEncryptionService().decrypt(keyValue);
527                     } catch (GeneralSecurityException e) {
528                         throw new RuntimeException(e);
529                     }
530                 }
531 
532                 parameterValues.put(keyPropertyName, keyValue);
533             }
534         }
535 
536         return parameterValues;
537     }
538 
539     /**
540      * Builds a Map containing a key/value pair for each property given in the property names list, general
541      * security is checked to determine if the value needs to be encrypted along with applying formatting to
542      * the value
543      *
544      * @param propertyNames - list of property names to get key/value pairs for
545      * @param dataObject - object instance containing the properties for which the values will be pulled
546      * @return Map<String, String> containing entry for each property name with the property name as the map key
547      * and the property value as the value
548      */
549     public static Map<String, String> getPropertyKeyValuesFromDataObject(List<String> propertyNames,
550             Object dataObject) {
551         return getPropertyKeyValuesFromDataObject(propertyNames, Collections.<String>emptyList(), dataObject);
552     }
553 
554     /**
555      * Builds a Map containing a key/value pair for each property given in the property names list, general
556      * security is checked to determine if the value needs to be encrypted along with applying formatting to
557      * the value
558      *
559      * @param propertyNames - list of property names to get key/value pairs for
560      * @param securePropertyNames - list of secure property names to match for encryption
561      * @param dataObject - object instance containing the properties for which the values will be pulled
562      * @return Map<String, String> containing entry for each property name with the property name as the map key
563      * and the property value as the value
564      */
565     public static Map<String, String> getPropertyKeyValuesFromDataObject(List<String> propertyNames,
566             List<String> securePropertyNames, Object dataObject) {
567         Map<String, String> propertyKeyValues = new HashMap<String, String>();
568 
569         if (dataObject == null) {
570             return propertyKeyValues;
571         }
572 
573         // iterate through properties and add a map entry for each
574         for (String propertyName : propertyNames) {
575             Object propertyValue = ObjectPropertyUtils.getPropertyValue(dataObject, propertyName);
576             if (propertyValue == null) {
577                 propertyValue = StringUtils.EMPTY;
578             }
579 
580             // secure values are not returned
581             if (!isSecure(propertyName, securePropertyNames, dataObject, propertyValue)) {
582                 propertyKeyValues.put(propertyName, propertyValue.toString());
583             }
584 
585         }
586 
587         return propertyKeyValues;
588     }
589 
590     /**
591      * Determines whether a property name should be secured, either based on installed sensitive data patterns, a list
592      * of secure property name patterns, or attributes in the Data Dictionary.
593      *
594      * @param propertyName The property name to check for security
595      * @param securePropertyNames The secure property name patterns to check
596      * @param dataObject The object containing this property
597      * @param propertyValue The value of the property
598      * @return true if the property needs to be secure, false otherwise
599      */
600     private static boolean isSecure(String propertyName, List<String> securePropertyNames, Object dataObject,
601             Object propertyValue) {
602         if (propertyValue instanceof String && containsSensitiveDataPatternMatch((String) propertyValue)) {
603             return true;
604         }
605 
606         if (containsSecurePropertyName(propertyName, securePropertyNames)) {
607             return true;
608         }
609 
610         return KRADServiceLocatorWeb.getDataObjectAuthorizationService()
611                 .attributeValueNeedsToBeEncryptedOnFormsAndLinks(dataObject.getClass(), propertyName);
612     }
613 
614     /**
615      * Helper method to identify if propertyName contains a secure property name element.
616      * Check handles simple or compound names and ignores partial matches.
617      *
618      * @param propertyName property name as a single term or compound term (i.e. items[0].propertyName)
619      * @param securePropertyNames list of secure property names to match
620      * @return true if any of the secure property names are found in the property name, false otherwise
621      */
622     private static boolean containsSecurePropertyName(String propertyName, List<String> securePropertyNames) {
623         if (securePropertyNames == null) {
624             return false;
625         }
626 
627         for (String securePropertyName : securePropertyNames) {
628             // pattern prefix and suffix used to handle compound names and ignore partial name matches
629             if (Pattern.compile("(?:\\.|^)" + Pattern.quote(securePropertyName) + "(?:\\.|\\[|$)").matcher(propertyName)
630                     .find()) {
631                 return true;
632             }
633         }
634 
635         return false;
636     }
637 
638     /**
639      * Utility method to convert a Map to a Properties object
640      *
641      * @param parameters - map to convert
642      * @return Properties object containing all the map entries
643      */
644     public static Properties convertMapToProperties(Map<String, String> parameters) {
645         Properties properties = new Properties();
646 
647         if (parameters != null) {
648             for (Map.Entry<String, String> parameter : parameters.entrySet()) {
649                 properties.put(parameter.getKey(), parameter.getValue());
650             }
651         }
652 
653         return properties;
654     }
655 
656     /**
657      * Utility method to convert a Request Parameters Map to a Properties object
658      *
659      * <p>
660      * Multiple values for a parameter are joined together with comma delimiter
661      * </p>
662      *
663      * @param requestParameters - map to convert
664      * @return Properties object containing all the map entries
665      */
666     public static Properties convertRequestMapToProperties(Map<String, String[]> requestParameters) {
667         Properties properties = new Properties();
668 
669         if (requestParameters != null) {
670             for (Map.Entry<String, String[]> parameter : requestParameters.entrySet()) {
671                 String[] parameterValue = parameter.getValue();
672                 String parameterValueString = StringUtils.join(parameterValue, ",");
673 
674                 properties.put(parameter.getKey(), parameterValueString);
675             }
676         }
677 
678         return properties;
679     }
680 
681     /**
682      * This method converts request parameters coming in as String to native types in case of Boolean, Number or
683      * java.sql.Date.
684      * For boolean the conversion is performed only if an @Converter annotation is set on the JPA entity field.
685      *
686      * @param dataObjectClass - business object class
687      * @param parameters - map of request parameters with field values as String for the fields in the dataObjectClass
688      * @return Map <String,Object> converted values
689      */
690     public static Map<String, Object> coerceRequestParameterTypes(Class<?> dataObjectClass,
691             Map<String, String> parameters) {
692         Map<String, Object> filteredFieldValues = new HashMap<String, Object>();
693         List<java.lang.reflect.Field> allFields = ObjectPropertyUtils.getAllFields(
694                 new ArrayList<java.lang.reflect.Field>(), dataObjectClass, Object.class);
695 
696         for (String fieldName : parameters.keySet()) {
697             Class<?> propertyType = ObjectPropertyUtils.getPropertyType(dataObjectClass, fieldName);
698 
699             String strValue = parameters.get(fieldName);
700 
701             if (TypeUtils.isBooleanClass(propertyType) && isConvertAnnotationPresent(allFields, fieldName)) {
702                 filteredFieldValues.put(fieldName, Truth.strToBooleanIgnoreCase(strValue));
703             } else if (TypeUtils.isIntegralClass(propertyType) || TypeUtils.isDecimalClass(propertyType)) {
704                 try {
705                     filteredFieldValues.put(fieldName, hydrateAttributeValue(propertyType, strValue));
706                 } catch (Exception nfe) {
707                     GlobalVariables.getMessageMap().putError("parameters[" + fieldName + "]",
708                             RiceKeyConstants.ERROR_NUMBER, strValue);
709                     throw new RuntimeException("Could not parse property value into Number for " + fieldName);
710                 }
711             } else if (TypeUtils.isTemporalClass(propertyType)) {
712                 try {
713                     filteredFieldValues.put(fieldName, CoreApiServiceLocator.getDateTimeService().convertToSqlDate(
714                             strValue));
715                 } catch (ParseException pe) {
716                     GlobalVariables.getMessageMap().putError("parameters[" + fieldName + "]",
717                             RiceKeyConstants.ERROR_DATE_TIME, strValue);
718                     throw new RuntimeException("Could not parse property value into java.sql.Date for " + fieldName);
719                 }
720             }
721 
722             // If value not converted set the value from parameters
723             if (filteredFieldValues.get(fieldName) == null) {
724                 filteredFieldValues.put(fieldName, parameters.get(fieldName));
725             }
726 
727         }
728         return filteredFieldValues;
729 
730     }
731 
732     /**
733      * Checks to see if the specified field from the list of allFields has the @Convert annotation set on it
734      *
735      * @param allFields List of all fields on the entity
736      * @param fieldName Field name to check for @Convert annotation
737      * @return true if annotation is present else false
738      */
739     private static boolean isConvertAnnotationPresent(List<java.lang.reflect.Field> allFields, String fieldName) {
740         //Check if there is a @Convert annotation on the field
741         boolean convertAnnotationFound = false;
742         for (java.lang.reflect.Field f : allFields) {
743             if (f.getName().equalsIgnoreCase(fieldName)) {
744                 if (f.getAnnotation(javax.persistence.Convert.class) != null) {
745                     convertAnnotationFound = true;
746                 }
747                 break;
748             }
749         }
750 
751         return convertAnnotationFound;
752     }
753 
754     /**
755      * Check if data might be sensitive
756      *
757      * <p>
758      * The sensitivity of the data is checked by matching it against the sensitive data patterns that are specified
759      * in the system parameter table.
760      * </p>
761      *
762      * @param fieldValue data to be checked for sensitivity
763      * @return true if the data matches the sensitive data pattern, false otherwise
764      */
765     public static boolean containsSensitiveDataPatternMatch(String fieldValue) {
766         if (StringUtils.isBlank(fieldValue)) {
767             return false;
768         }
769 
770         ParameterService parameterService = CoreFrameworkServiceLocator.getParameterService();
771         Collection<String> sensitiveDataPatterns = parameterService.getParameterValuesAsString(
772                 KRADConstants.KNS_NAMESPACE, ParameterConstants.ALL_COMPONENT,
773                 KRADConstants.SystemGroupParameterNames.SENSITIVE_DATA_PATTERNS);
774 
775         for (String pattern : sensitiveDataPatterns) {
776             if (Pattern.compile(pattern).matcher(fieldValue).find()) {
777                 return true;
778             }
779         }
780 
781         return false;
782     }
783 
784     /**
785      * Strips out common patterns used in cross side scripting.
786      *
787      * @param values string array to strip patterns from
788      * @return cleaned string array
789      */
790     public static String[] stripXSSPatterns(String[] values) {
791         ArrayList<String> strippedValues = new ArrayList<String>();
792 
793         for (String value : values) {
794             strippedValues.add(stripXSSPatterns(value));
795         }
796 
797         return strippedValues.toArray(new String[]{});
798     }
799 
800     /**
801      * Strips out common patterns used in cross side scripting.
802      *
803      * @param value string to strip patterns from
804      * @return cleaned string
805      */
806     public static String stripXSSPatterns(String value) {
807         if (value == null) {
808             return null;
809         }
810 
811         // Avoid null characters
812         value = value.replaceAll("", "");
813 
814         // Avoid anything between script tags
815         Pattern scriptPattern = Pattern.compile("<script>(.*?)</script>", Pattern.CASE_INSENSITIVE);
816         value = scriptPattern.matcher(value).replaceAll("");
817 
818         // Avoid anything in a src='...' type of expression
819         scriptPattern = Pattern.compile("src[\r\n]*=[\r\n]*\\\'(.*?)\\\'",
820                 Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
821         value = scriptPattern.matcher(value).replaceAll("");
822 
823         scriptPattern = Pattern.compile("src[\r\n]*=[\r\n]*\\\"(.*?)\\\"",
824                 Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
825         value = scriptPattern.matcher(value).replaceAll("");
826 
827         // Remove any lonesome </script> tag
828         scriptPattern = Pattern.compile("</script>", Pattern.CASE_INSENSITIVE);
829         value = scriptPattern.matcher(value).replaceAll("");
830 
831         // Remove any lonesome <script ...> tag
832         scriptPattern = Pattern.compile("<script(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
833         value = scriptPattern.matcher(value).replaceAll("");
834 
835         // Avoid eval(...) expressions
836         scriptPattern = Pattern.compile("eval\\((.*?)\\)",
837                 Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
838         value = scriptPattern.matcher(value).replaceAll("");
839 
840         // Avoid expression(...) expressions
841         scriptPattern = Pattern.compile("expression\\((.*?)\\)",
842                 Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
843         value = scriptPattern.matcher(value).replaceAll("");
844 
845         // Avoid javascript:... expressions
846         scriptPattern = Pattern.compile("javascript:", Pattern.CASE_INSENSITIVE);
847         value = scriptPattern.matcher(value).replaceAll("");
848 
849         // Avoid vbscript:... expressions
850         scriptPattern = Pattern.compile("vbscript:", Pattern.CASE_INSENSITIVE);
851         value = scriptPattern.matcher(value).replaceAll("");
852 
853         // Avoid onload= expressions
854         scriptPattern = Pattern.compile("onload(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
855         value = scriptPattern.matcher(value).replaceAll("");
856 
857         return value;
858     }
859 
860     /**
861      * Gets the UserSession object from the HttpServletRequest object's
862      * associated session.
863      *
864      * <p>
865      * In some cases (different threads) the UserSession cannot be retrieved
866      * from GlobalVariables but can still be accessed via the session object
867      * </p>
868      *
869      * @param request servlet request
870      * @return user session found in the request's servlet session
871      */
872     public static final UserSession getUserSessionFromRequest(HttpServletRequest request) {
873         return (UserSession) request.getSession().getAttribute(KRADConstants.USER_SESSION_KEY);
874     }
875 
876     /**
877      * Check if current deployment is the production environment
878      *
879      * @return true if the deploy environment is production, false otherwise
880      */
881     public static boolean isProductionEnvironment() {
882         return CoreApiServiceLocator.getKualiConfigurationService().getPropertyValueAsString(
883                 KRADConstants.PROD_ENVIRONMENT_CODE_KEY).equalsIgnoreCase(
884                 CoreApiServiceLocator.getKualiConfigurationService().getPropertyValueAsString(
885                         KRADConstants.ENVIRONMENT_KEY));
886     }
887 
888     /**
889      * Gets the message associated with ErrorMessage object passed in, using message service.
890      * The prefix and suffix will be appended to the retrieved message if processPrefixSuffix is true and if those
891      * settings are set on the ErrorMessage passed in.
892      *
893      * @param errorMessage the ErrorMessage object containing the message key(s)
894      * @param processPrefixSuffix if true appends the prefix and suffix to the message if they exist on ErrorMessage
895      * @return the converted/retrieved message
896      */
897     public static String getMessageText(ErrorMessage errorMessage, boolean processPrefixSuffix) {
898         String message = "";
899         if (errorMessage != null && errorMessage.getErrorKey() != null) {
900             MessageService messageService = KRADServiceLocatorWeb.getMessageService();
901 
902             // find message by key
903             message = messageService.getMessageText(errorMessage.getNamespaceCode(), errorMessage.getComponentCode(),
904                     errorMessage.getErrorKey());
905             if (message == null) {
906                 message = "Intended message with key: " + errorMessage.getErrorKey() + " not found.";
907             }
908 
909             if (errorMessage.getMessageParameters() != null && StringUtils.isNotBlank(message)) {
910                 message = message.replace("'", "''");
911                 message = MessageFormat.format(message, (Object[]) errorMessage.getMessageParameters());
912             }
913 
914             // add prefix
915             if (StringUtils.isNotBlank(errorMessage.getMessagePrefixKey()) && processPrefixSuffix) {
916                 String prefix = messageService.getMessageText(errorMessage.getNamespaceCode(),
917                         errorMessage.getComponentCode(), errorMessage.getMessagePrefixKey());
918 
919                 if (errorMessage.getMessagePrefixParameters() != null && StringUtils.isNotBlank(prefix)) {
920                     prefix = prefix.replace("'", "''");
921                     prefix = MessageFormat.format(prefix, (Object[]) errorMessage.getMessagePrefixParameters());
922                 }
923 
924                 if (StringUtils.isNotBlank(prefix)) {
925                     message = prefix + " " + message;
926                 }
927             }
928 
929             // add suffix
930             if (StringUtils.isNotBlank(errorMessage.getMessageSuffixKey()) && processPrefixSuffix) {
931                 String suffix = messageService.getMessageText(errorMessage.getNamespaceCode(),
932                         errorMessage.getComponentCode(), errorMessage.getMessageSuffixKey());
933 
934                 if (errorMessage.getMessageSuffixParameters() != null && StringUtils.isNotBlank(suffix)) {
935                     suffix = suffix.replace("'", "''");
936                     suffix = MessageFormat.format(suffix, (Object[]) errorMessage.getMessageSuffixParameters());
937                 }
938 
939                 if (StringUtils.isNotBlank(suffix)) {
940                     message = message + " " + suffix;
941                 }
942             }
943         }
944 
945         return message;
946     }
947 
948     /**
949      * Logs the error messages if any in the message map
950      */
951     public static void logErrors() {
952 
953         if (!GlobalVariables.getMessageMap().hasErrors()) {
954             return;
955         }
956 
957         for (Iterator<Map.Entry<String, List<ErrorMessage>>> i =
958                      GlobalVariables.getMessageMap().getAllPropertiesAndErrors().iterator(); i.hasNext(); ) {
959             Map.Entry<String, List<ErrorMessage>> e = i.next();
960 
961             StringBuffer logMessage = buildMessage(e);
962             LOG.error(logMessage);
963         }
964     }
965 
966     /**
967      * Builds the message for a given entry in the messageMap. The entry could have multiple messages for a given key.
968      * The messages are appended separated by a ;
969      *
970      * @param e Map entry of property and errors for that property
971      * @return logMessage
972      */
973     private static StringBuffer buildMessage(Map.Entry<String, List<ErrorMessage>> e) {
974         StringBuffer logMessage = new StringBuffer();
975         logMessage.append("[" + e.getKey() + "] ");
976         boolean first = true;
977 
978         List<ErrorMessage> errorList = e.getValue();
979         for (Iterator<ErrorMessage> j = errorList.iterator(); j.hasNext(); ) {
980             ErrorMessage em = j.next();
981 
982             // if its the first message for the key
983             if (first) {
984                 first = false;
985             } else {
986                 logMessage.append(";");
987             }
988             logMessage.append(em);
989         }
990         return logMessage;
991     }
992 
993     /**
994      * Generate the request parameter portion of the url based on the map of key value pairs passed in
995      *
996      * @param requestParameters the request parameters to use in the string
997      * @return a request parameter string starting with "?" with "&" separators, or blank if the mapped passed in is
998      * blank
999      */
1000     public static String getRequestStringFromMap(Map<String, String> requestParameters) {
1001         String requestString = "";
1002 
1003         if (requestParameters.isEmpty()) {
1004             return requestString;
1005         }
1006 
1007         URLCodec urlCodec = new URLCodec(KRADConstants.DEFAULT_ENCODING);
1008 
1009         for (String key : requestParameters.keySet()) {
1010             String value = null;
1011             try {
1012                 value = urlCodec.encode(requestParameters.get(key));
1013             } catch (EncoderException e) {
1014                 throw new RuntimeException("Unable to encode parameter name or value: " + key + "=" + value, e);
1015             }
1016 
1017             if (StringUtils.isNotBlank(requestString)) {
1018                 requestString = requestString + "&";
1019             }
1020 
1021             requestString = requestString + key + "=" + value;
1022         }
1023 
1024         return "?" + requestString;
1025     }
1026 
1027     /**
1028      * Adds the header and content of an attachment to the response.
1029      *
1030      * @param response HttpServletResponse instance
1031      * @param contentType the content type of the attachment
1032      * @param inputStream the content of the attachment
1033      * @param fileName the file name of the attachment
1034      * @param fileSize the size of the attachment
1035      * @throws IOException if attachment to the results fails due to an IO error
1036      */
1037     public static void addAttachmentToResponse(HttpServletResponse response, InputStream inputStream,
1038             String contentType, String fileName, long fileSize) throws IOException {
1039 
1040         // If there are quotes in the name, we should replace them to avoid issues.
1041         // The filename will be wrapped with quotes below when it is set in the header
1042         String updateFileName;
1043         if (fileName.contains("\"")) {
1044             updateFileName = fileName.replaceAll("\"", "");
1045         } else {
1046             updateFileName = fileName;
1047         }
1048 
1049         // set response
1050         response.setContentType(contentType);
1051         response.setContentLength(org.springframework.util.NumberUtils.convertNumberToTargetClass(fileSize,
1052                 Integer.class));
1053         response.setHeader("Content-disposition", "attachment; filename=\"" + updateFileName + "\"");
1054         response.setHeader("Expires", "0");
1055         response.setHeader("Cache-Control", "must-revalidate, post-check=0, pre-check=0");
1056         response.setHeader("Pragma", "public");
1057 
1058         // Copy the input stream to the response
1059         FileCopyUtils.copy(inputStream, response.getOutputStream());
1060     }
1061 
1062     /**
1063      * Helper method for building a URL that will invoke the given controller and render the given
1064      * KRAD view
1065      *
1066      * @param baseUrl base url (domain, port)
1067      * @param controllerMapping mapping for the controller that should be invoked
1068      * @param viewId id for the view that should be rendered
1069      * @return url for invoking the view
1070      */
1071     public static String buildViewUrl(String baseUrl, String controllerMapping, String viewId) {
1072         Assert.hasLength(baseUrl, "base url is null or empty");
1073         Assert.hasLength(controllerMapping, "controller mapping is null or empty");
1074         Assert.hasLength(viewId, "view id is null or empty");
1075 
1076         StringBuffer url = new StringBuffer();
1077 
1078         url.append(baseUrl);
1079 
1080         if (!baseUrl.endsWith("/")) {
1081             url.append("/");
1082         }
1083 
1084         url.append(controllerMapping);
1085 
1086         url.append("?");
1087         url.append(UifParameters.VIEW_ID);
1088         url.append("=");
1089         url.append(viewId);
1090 
1091         return url.toString();
1092     }
1093 
1094     /**
1095      * Removes parameters from the given properties object that are request specific (useful when manupulating the
1096      * current URL to invoke something else)
1097      *
1098      * @param requestParameters properties instance containing the parameters to clean
1099      */
1100     public static void cleanRequestParameters(Properties requestParameters) {
1101         requestParameters.remove(UifParameters.SESSION_ID);
1102         requestParameters.remove(UifParameters.AJAX_REQUEST);
1103         requestParameters.remove(UifParameters.AJAX_RETURN_TYPE);
1104         requestParameters.remove(UifParameters.FORM_KEY);
1105         requestParameters.remove(UifParameters.JUMP_TO_ID);
1106         requestParameters.remove(UifParameters.FOCUS_ID);
1107     }
1108 
1109     /**
1110      * Get the full url for a request (requestURL + queryString)
1111      *
1112      * @param request the request
1113      * @return the fullUrl
1114      */
1115     public static String getFullURL(HttpServletRequest request) {
1116         if (request == null) {
1117             return null;
1118         }
1119 
1120         StringBuffer requestURL = request.getRequestURL();
1121         String queryString = request.getQueryString();
1122 
1123         if (queryString == null) {
1124             return requestURL.toString();
1125         } else {
1126             return requestURL.append('?').append(queryString).toString();
1127         }
1128     }
1129 
1130     /**
1131      * Determines whether two URLs point at the same domain.
1132      *
1133      * @param firstDomain first URL string to compare
1134      * @param secondDomain second URL string to compare
1135      * @return true if the domains are different, false otherwise
1136      */
1137     public static boolean areDifferentDomains(String firstDomain, String secondDomain) {
1138         try {
1139             URL urlOne = new URL(firstDomain.toLowerCase());
1140             URL urlTwo = new URL(secondDomain.toLowerCase());
1141 
1142             if (urlOne.getHost().equals(urlTwo.getHost())) {
1143                 LOG.debug("Hosts "
1144                         + urlOne.getHost()
1145                         + " of domains "
1146                         + firstDomain
1147                         + " and "
1148                         + secondDomain
1149                         + " were determined to be equal");
1150 
1151                 return false;
1152             } else {
1153                 LOG.debug("Hosts "
1154                         + urlOne.getHost()
1155                         + " of domains "
1156                         + firstDomain
1157                         + " and "
1158                         + secondDomain
1159                         + " are not equal");
1160 
1161                 return true;
1162             }
1163         } catch (MalformedURLException mue) {
1164             LOG.error("Unable to successfully compare domains " + firstDomain + " and " + secondDomain);
1165         }
1166 
1167         return true;
1168     }
1169 
1170     /**
1171      * Attempts to generate a unique view title by combining the View's headerText with the title attribute for the
1172      * dataObjectClass found through the DataObjectMetaDataService.  If the title attribute cannot be found, just the
1173      * headerText is returned.
1174      *
1175      * @param form the form
1176      * @param view the view
1177      * @return the headerText with the title attribute in parenthesis or just the headerText if it title attribute
1178      * cannot be determined
1179      */
1180     public static String generateUniqueViewTitle(UifFormBase form, View view) {
1181         String title = view.getHeader().getHeaderText();
1182 
1183         String viewLabelPropertyName = "";
1184 
1185         Class<?> dataObjectClass;
1186         if (StringUtils.isNotBlank(view.getDefaultBindingObjectPath())) {
1187             dataObjectClass = ObjectPropertyUtils.getPropertyType(form, view.getDefaultBindingObjectPath());
1188         } else {
1189             dataObjectClass = view.getFormClass();
1190         }
1191 
1192         if (dataObjectClass != null) {
1193             viewLabelPropertyName = KRADServiceLocatorWeb.getLegacyDataAdapter().getTitleAttribute(dataObjectClass);
1194         }
1195 
1196         String viewLabelPropertyPath = "";
1197         if (StringUtils.isNotBlank(viewLabelPropertyName)) {
1198             // adjust binding prefix
1199             if (!viewLabelPropertyName.startsWith(UifConstants.NO_BIND_ADJUST_PREFIX)) {
1200                 if (StringUtils.isNotBlank(view.getDefaultBindingObjectPath())) {
1201                     viewLabelPropertyPath = view.getDefaultBindingObjectPath() + "." + viewLabelPropertyName;
1202                 }
1203             } else {
1204                 viewLabelPropertyPath = StringUtils.removeStart(viewLabelPropertyName,
1205                         UifConstants.NO_BIND_ADJUST_PREFIX);
1206             }
1207         } else {
1208             // attempt to get title attribute
1209             if (StringUtils.isNotBlank(view.getDefaultBindingObjectPath())) {
1210                 dataObjectClass = ViewModelUtils.getObjectClassForMetadata(view, form,
1211                         view.getDefaultBindingObjectPath());
1212             } else {
1213                 dataObjectClass = view.getFormClass();
1214             }
1215 
1216             if (dataObjectClass != null) {
1217                 String titleAttribute = KRADServiceLocatorWeb.getLegacyDataAdapter().getTitleAttribute(dataObjectClass);
1218                 if (StringUtils.isNotBlank(titleAttribute)) {
1219                     viewLabelPropertyPath = view.getDefaultBindingObjectPath() + "." + titleAttribute;
1220                 }
1221             }
1222         }
1223 
1224         Object viewLabelPropertyValue = null;
1225         if (StringUtils.isNotBlank(viewLabelPropertyPath) && ObjectPropertyUtils.isReadableProperty(form,
1226                 viewLabelPropertyPath)) {
1227             viewLabelPropertyValue = ObjectPropertyUtils.getPropertyValue(form, viewLabelPropertyPath);
1228         }
1229 
1230         if (viewLabelPropertyValue != null && StringUtils.isNotBlank(viewLabelPropertyValue.toString()) && StringUtils
1231                 .isNotBlank(title)) {
1232             return title + " (" + viewLabelPropertyValue.toString() + ")";
1233         } else {
1234             return title;
1235         }
1236     }
1237 
1238     /**
1239      * Helper method for building title text for an element and a map of key/value pairs,
1240      *
1241      * <p>
1242      * Each key of the key value map is assumed to be an attribute for the given element class. The label is then
1243      * retrieved for the attribute from the data dictionary and used in the title (instead of the key)
1244      * </p>
1245      *
1246      * @param prependText text to prepend to the title
1247      * @param element element class the title is being generated for, used as the parent for getting the key labels
1248      * @param keyValueMap map of key value pairs to add to the title text
1249      * @return title string
1250      */
1251     public static String buildAttributeTitleString(String prependText, Class<?> element,
1252             Map<String, String> keyValueMap) {
1253         StringBuffer titleText = new StringBuffer(prependText);
1254 
1255         for (String key : keyValueMap.keySet()) {
1256             String fieldVal = keyValueMap.get(key).toString();
1257 
1258             titleText.append(" "
1259                     + KRADServiceLocatorWeb.getDataDictionaryService().getAttributeLabel(element, key)
1260                     + "="
1261                     + fieldVal.toString());
1262         }
1263 
1264         return titleText.toString();
1265     }
1266 
1267     /**
1268      * Attempts to extract a string value out of the field passed in, varies depending on field type
1269      *
1270      * <p>If the field is a dataField, it will use its propertyName to retrieve a value, otherwise it will try to
1271      * retrieve textual content out of various component types.  If the field is a FieldGroup, only the first
1272      * component's determined value will be used.  This function is used for sorting.</p>
1273      *
1274      * @param model the current model
1275      * @param field the field to get a value from
1276      * @return the field's String value, false if it cant be determined
1277      */
1278     public static String getSimpleFieldValue(Object model, Field field) {
1279         if (field == null) {
1280             return null;
1281         }
1282 
1283         String value = null;
1284         // check for what type of field this is
1285         if (field instanceof DataField) {
1286             String propertyPath = ((DataField) field).getBindingInfo().getBindingPath();
1287             Object valueObject = null;
1288 
1289             if (field.isHidden()) {
1290                 return "";
1291             }
1292 
1293             // check if readable
1294             if (ObjectPropertyUtils.isReadableProperty(model, propertyPath)) {
1295                 valueObject = ObjectPropertyUtils.getPropertyValue(model, propertyPath);
1296             }
1297 
1298             // use object's string value
1299             if (valueObject != null && !((DataField) field).isApplyMask()) {
1300                 value = valueObject.toString();
1301             } else if (valueObject != null && ((DataField) field).isApplyMask()) {
1302                 value = ((DataField) field).getMaskFormatter().maskValue(valueObject);
1303             }
1304         } else if (field instanceof ActionField) {
1305             value = ((ActionField) field).getActionLabel();
1306 
1307             // use image alt text if any
1308             if (StringUtils.isBlank(value) && ((ActionField) field).getActionImage() != null) {
1309                 value = ((ActionField) field).getActionImage().getAltText();
1310             }
1311         } else if (field instanceof LinkField) {
1312             value = ((LinkField) field).getLinkText();
1313         } else if (field instanceof ImageField) {
1314             value = ((ImageField) field).getAltText();
1315         } else if (field instanceof MessageField && ((MessageField) field).getMessage() != null) {
1316             value = ((MessageField) field).getMessage().getMessageText();
1317         } else if (field instanceof SpaceField) {
1318             value = "";
1319         } else if (field instanceof FieldGroup
1320                 && ((FieldGroup) field).getGroup() != null
1321                 && ((FieldGroup) field).getGroup().getItems() != null
1322                 && !((FieldGroup) field).getGroup().getItems().isEmpty()) {
1323             // using first components type for assumed value
1324             Component firstComponent = ((FieldGroup) field).getGroup().getItems().get(0);
1325 
1326             // check first component type to extract value
1327             if (firstComponent != null && firstComponent instanceof Field) {
1328                 value = getSimpleFieldValue(model, field);
1329             } else if (firstComponent instanceof Action && StringUtils.isNotBlank(
1330                     ((Action) firstComponent).getActionLabel())) {
1331                 value = ((Action) firstComponent).getActionLabel();
1332             } else if (firstComponent instanceof Action && ((Action) firstComponent).getActionImage() != null) {
1333                 value = ((Action) firstComponent).getActionImage().getAltText();
1334             } else if (firstComponent instanceof Link) {
1335                 value = ((Link) firstComponent).getLinkText();
1336             } else if (firstComponent instanceof Image) {
1337                 value = ((Image) firstComponent).getAltText();
1338             } else if (firstComponent instanceof org.kuali.rice.krad.uif.element.Message) {
1339                 value = ((org.kuali.rice.krad.uif.element.Message) firstComponent).getMessageText();
1340             } else {
1341                 value = null;
1342             }
1343         }
1344 
1345         return value;
1346     }
1347 
1348     /**
1349      * Helper method to change common characters into HTML attribute safe characters
1350      *
1351      * @param message the string to convert
1352      * @return the converted string with quotes, sing quotes, and slash replaced
1353      */
1354     public static String convertToHTMLAttributeSafeString(String message) {
1355         if (StringUtils.isBlank(message)) {
1356             return message;
1357         }
1358 
1359         if (message.contains("\"")) {
1360             message = message.replace("\"", "&quot;");
1361         }
1362         if (message.contains("'")) {
1363             message = message.replace("'", "&#39;");
1364         }
1365         if (message.contains("\\")) {
1366             message = message.replace("\\", "&#92;");
1367         }
1368 
1369         return message;
1370     }
1371 
1372     /**
1373      * Get the rowCss for the line specified, by evaluating the conditionalRowCssClasses map for that row
1374      *
1375      * @param conditionalRowCssClasses the conditionalRowCssClass map, where key is the condition and value is
1376      * the class(es)
1377      * @param lineIndex the line/row index
1378      * @param isOdd true if the row is considered odd
1379      * @param lineContext the lineContext for expressions, pass null if not applicable
1380      * @param expressionEvaluator the expressionEvaluator, pass null if not applicable
1381      * @return row csss class String for the class attribute of this row
1382      */
1383     public static String generateRowCssClassString(Map<String, String> conditionalRowCssClasses, int lineIndex,
1384             boolean isOdd, Map<String, Object> lineContext, ExpressionEvaluator expressionEvaluator) {
1385         String rowCss = "";
1386         if (conditionalRowCssClasses == null || conditionalRowCssClasses.isEmpty()) {
1387             return rowCss;
1388         }
1389 
1390         for (String cssRule : conditionalRowCssClasses.keySet()) {
1391             if (cssRule.startsWith(UifConstants.EL_PLACEHOLDER_PREFIX) && lineContext != null &&
1392                     expressionEvaluator != null) {
1393                 String outcome = expressionEvaluator.evaluateExpressionTemplate(lineContext, cssRule);
1394                 if (outcome != null && Boolean.parseBoolean(outcome)) {
1395                     rowCss = rowCss + " " + conditionalRowCssClasses.get(cssRule);
1396                 }
1397             } else if (cssRule.equals(UifConstants.RowSelection.ALL)) {
1398                 rowCss = rowCss + " " + conditionalRowCssClasses.get(cssRule);
1399             } else if (cssRule.equals(UifConstants.RowSelection.EVEN) && !isOdd) {
1400                 rowCss = rowCss + " " + conditionalRowCssClasses.get(cssRule);
1401             } else if (cssRule.equals(UifConstants.RowSelection.ODD) && isOdd) {
1402                 rowCss = rowCss + " " + conditionalRowCssClasses.get(cssRule);
1403             } else if (StringUtils.isNumeric(cssRule) && (lineIndex + 1) == Integer.parseInt(cssRule)) {
1404                 rowCss = rowCss + " " + conditionalRowCssClasses.get(cssRule);
1405             }
1406         }
1407 
1408         rowCss = StringUtils.removeStart(rowCss, " ");
1409 
1410         return rowCss;
1411     }
1412 
1413     /**
1414      * LegacyCase - This method simply uses PojoPropertyUtilsBean logic to get the Class of a Class property.
1415      * This method does not have any of the logic needed to obtain the Class of an element of a Collection specified in
1416      * the DataDictionary.
1417      *
1418      * @param object An instance of the Class of which we're trying to get the property Class.
1419      * @param propertyName The name of the property.
1420      * @return property type
1421      * @throws IllegalAccessException
1422      * @throws NoSuchMethodException
1423      * @throws InvocationTargetException
1424      */
1425     static public Class easyGetPropertyType(Object object,
1426             String propertyName) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
1427         if (LegacyUtils.useLegacyForObject(object)) {
1428             return PropertyUtils.getPropertyType(object, propertyName);
1429         }
1430         return KradDataServiceLocator.getDataObjectService().wrap(object).getPropertyType(propertyName);
1431     }
1432 
1433     /**
1434      * Sets the property of an object with the given value. Converts using the formatter of the type for the property.
1435      * Note: propertyType does not need passed, is found by util method.
1436      *
1437      * @param bo business object
1438      * @param propertyName property name
1439      * @param propertyValue propery value
1440      * @throws IllegalAccessException
1441      * @throws InvocationTargetException
1442      * @throws NoSuchMethodException
1443      */
1444     public static void setObjectProperty(Object bo, String propertyName,
1445             Object propertyValue) throws FormatException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
1446         Class propertyType = easyGetPropertyType(bo, propertyName);
1447         setObjectProperty(bo, propertyName, propertyType, propertyValue);
1448 
1449     }
1450 
1451     /**
1452      * Sets the property of an object with the given value. Converts using the formatter of the given type if one is
1453      * found.
1454      *
1455      * @param bo
1456      * @param propertyName
1457      * @param propertyType
1458      * @param propertyValue
1459      * @throws NoSuchMethodException
1460      * @throws InvocationTargetException
1461      * @throws IllegalAccessException
1462      */
1463     public static void setObjectProperty(Object bo, String propertyName, Class propertyType,
1464             Object propertyValue) throws FormatException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
1465         KRADServiceLocatorWeb.getLegacyDataAdapter().setObjectProperty(bo, propertyName, propertyType, propertyValue);
1466     }
1467 
1468     /**
1469      * Removes all query characters from a string.
1470      *
1471      * @param string
1472      * @return Cleaned string
1473      */
1474     public static String clean(String string) {
1475         for (SearchOperator op : SearchOperator.QUERY_CHARACTERS) {
1476             string = StringUtils.replace(string, op.op(), KRADConstants.EMPTY_STRING);
1477         }
1478         return string;
1479     }
1480 
1481     /**
1482      * This method is a confirms if object is null, and unproxies if necessary to determine this
1483      *
1484      * @param object - any object, proxied or not, materialized or not
1485      * @return true if the object (or underlying materialized object) is null, false otherwise
1486      */
1487     public static boolean isNull(Object object) {
1488         if (object == null) {
1489             return true;
1490         }
1491         return KRADServiceLocatorWeb.getLegacyDataAdapter().isNull(object);
1492     }
1493 
1494     /**
1495      * This method is a confirms if object is not null, and unproxies if necessary to determine this
1496      *
1497      * @param object - any object, proxied or not, materialized or not
1498      * @return true if the object (or underlying materialized object) is not null, true if its null
1499      */
1500     public static boolean isNotNull(Object object) {
1501         return !isNull(object);
1502     }
1503 
1504     /**
1505      * Attempts to find the Class for the given potentially proxied object
1506      *
1507      * @param object the potentially proxied object to find the Class of
1508      * @return the best Class which could be found for the given object
1509      */
1510     public static Class materializeClassForProxiedObject(Object object) {
1511         return KRADServiceLocatorWeb.getLegacyDataAdapter().materializeClassForProxiedObject(object);
1512     }
1513 
1514     /**
1515      * This method runs the KRADUtils.isNotNull() method for each item in a list of BOs. KRADUtils.isNotNull() will
1516      * materialize
1517      * the objects if they are currently OJB proxies.
1518      *
1519      * @param possiblyProxiedObjects - a Collection of objects that may be proxies
1520      */
1521     public static void materializeObjects(Collection possiblyProxiedObjects) {
1522         for (Iterator i = possiblyProxiedObjects.iterator(); i.hasNext(); ) {
1523             KRADUtils.isNotNull(i.next());
1524         }
1525     }
1526 
1527     /**
1528      * Returns the prefix of a nested attribute name, or the empty string if the attribute name is not nested.
1529      *
1530      * @param attributeName
1531      * @return everything BEFORE the last "." character in attributeName
1532      */
1533     public static String getNestedAttributePrefix(String attributeName) {
1534         int lastIndex = PropertyAccessorUtils.getLastNestedPropertySeparatorIndex(attributeName);
1535 
1536         return lastIndex != -1 ? StringUtils.substring(attributeName, 0, lastIndex) : StringUtils.EMPTY;
1537     }
1538 
1539     /**
1540      * Returns the primitive part of an attribute name string.
1541      *
1542      * @param attributeName
1543      * @return everything AFTER the last "." character in attributeName
1544      */
1545     public static String getNestedAttributePrimitive(String attributeName) {
1546         int lastIndex = PropertyAccessorUtils.getLastNestedPropertySeparatorIndex(attributeName);
1547 
1548         return lastIndex != -1 ? StringUtils.substring(attributeName, lastIndex + 1) : attributeName;
1549     }
1550 
1551     /**
1552      * This method safely extracts either simple values OR nested values. For example, if the bo is SubAccount, and the
1553      * fieldName is
1554      * a21SubAccount.subAccountTypeCode, this thing makes sure it gets the value off the very end attribute, no matter
1555      * how deeply
1556      * nested it is. The code would be slightly simpler if this was done recursively, but this is safer, and consumes a
1557      * constant
1558      * amount of memory, no matter how deeply nested it goes.
1559      *
1560      * @param bo
1561      * @param fieldName
1562      * @return The field value if it exists. If it doesnt, and the name is invalid, and
1563      */
1564     public static Object getNestedValue(Object bo, String fieldName) {
1565         return KRADServiceLocatorWeb.getLegacyDataAdapter().getNestedValue(bo, fieldName);
1566     }
1567 
1568     /**
1569      * This method safely creates a object from a class
1570      * Convenience method to create new object and throw a runtime exception if it cannot
1571      * If the class is an {@link org.kuali.rice.krad.bo.ExternalizableBusinessObject}, this method will determine the
1572      * interface for the EBO and
1573      * query the
1574      * appropriate module service to create a new instance.
1575      *
1576      * @param clazz
1577      * @return a newInstance() of clazz
1578      */
1579     public static <T> T createNewObjectFromClass(Class<T> clazz) {
1580         if (clazz == null) {
1581             throw new RuntimeException("BO class was passed in as null");
1582         }
1583         return (T) KRADServiceLocatorWeb.getLegacyDataAdapter().createNewObjectFromClass(clazz);
1584     }
1585 
1586     private static KualiModuleService getKualiModuleService() {
1587         if (kualiModuleService == null) {
1588             kualiModuleService = KRADServiceLocatorWeb.getKualiModuleService();
1589         }
1590         return kualiModuleService;
1591     }
1592 
1593     /**
1594      * Updates the properties of the given component instance with the value found from the
1595      * corresponding map of client state (if found)
1596      *
1597      * @param component component instance to update
1598      * @param clientSideState map of state to sync with
1599      */
1600     public static void syncClientSideStateForComponent(Component component, Map<String, Object> clientSideState) {
1601         // find the map of state that was sent for component (if any)
1602         Map<String, Object> componentState = null;
1603         if (component instanceof View) {
1604             componentState = clientSideState;
1605         } else {
1606             if (clientSideState.containsKey(component.getId())) {
1607                 componentState = (Map<String, Object>) clientSideState.get(component.getId());
1608             }
1609         }
1610 
1611         // if state was sent, match with fields on the component that are annotated to have client state
1612         if ((componentState != null) && (!componentState.isEmpty())) {
1613             Map<String, Annotation> annotatedFields = CloneUtils.getFieldsWithAnnotation(component.getClass(),
1614                     ClientSideState.class);
1615 
1616             for (Map.Entry<String, Annotation> annotatedField : annotatedFields.entrySet()) {
1617                 ClientSideState clientSideStateAnnot = (ClientSideState) annotatedField.getValue();
1618 
1619                 String variableName = clientSideStateAnnot.variableName();
1620                 if (StringUtils.isBlank(variableName)) {
1621                     variableName = annotatedField.getKey();
1622                 }
1623 
1624                 if (componentState.containsKey(variableName)) {
1625                     Object value = componentState.get(variableName);
1626                     ObjectPropertyUtils.setPropertyValue(component, annotatedField.getKey(), value);
1627                 }
1628             }
1629         }
1630     }
1631 
1632 }