View Javadoc
1   /**
2    * Copyright 2005-2016 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.codec.EncoderException;
19  import org.apache.commons.codec.net.URLCodec;
20  import org.apache.commons.lang.StringUtils;
21  import org.kuali.rice.core.api.CoreApiServiceLocator;
22  import org.kuali.rice.core.api.encryption.EncryptionService;
23  import org.kuali.rice.core.api.util.Truth;
24  import org.kuali.rice.core.api.util.type.KualiDecimal;
25  import org.kuali.rice.core.web.format.BooleanFormatter;
26  import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
27  import org.kuali.rice.coreservice.framework.parameter.ParameterConstants;
28  import org.kuali.rice.coreservice.framework.parameter.ParameterService;
29  import org.kuali.rice.krad.UserSession;
30  import org.kuali.rice.krad.messages.Message;
31  import org.kuali.rice.krad.messages.MessageService;
32  import org.kuali.rice.krad.service.DataObjectMetaDataService;
33  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
34  import org.kuali.rice.krad.service.KualiModuleService;
35  import org.kuali.rice.krad.service.ModuleService;
36  import org.kuali.rice.krad.uif.UifConstants;
37  import org.kuali.rice.krad.uif.UifParameters;
38  import org.kuali.rice.krad.uif.component.Component;
39  import org.kuali.rice.krad.uif.element.Action;
40  import org.kuali.rice.krad.uif.element.Image;
41  import org.kuali.rice.krad.uif.element.Link;
42  import org.kuali.rice.krad.uif.field.ActionField;
43  import org.kuali.rice.krad.uif.field.DataField;
44  import org.kuali.rice.krad.uif.field.Field;
45  import org.kuali.rice.krad.uif.field.FieldGroup;
46  import org.kuali.rice.krad.uif.field.ImageField;
47  import org.kuali.rice.krad.uif.field.LinkField;
48  import org.kuali.rice.krad.uif.field.MessageField;
49  import org.kuali.rice.krad.uif.field.SpaceField;
50  import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
51  import org.kuali.rice.krad.uif.util.ViewModelUtils;
52  import org.kuali.rice.krad.uif.view.ExpressionEvaluator;
53  import org.kuali.rice.krad.uif.view.View;
54  import org.kuali.rice.krad.web.form.UifFormBase;
55  import org.springframework.util.Assert;
56  
57  import javax.servlet.ServletRequest;
58  import javax.servlet.http.HttpServletRequest;
59  import java.lang.reflect.Constructor;
60  import java.lang.reflect.Method;
61  import java.security.GeneralSecurityException;
62  import java.text.MessageFormat;
63  import java.text.NumberFormat;
64  import java.util.ArrayList;
65  import java.util.Arrays;
66  import java.util.Collection;
67  import java.util.Collections;
68  import java.util.HashMap;
69  import java.util.Iterator;
70  import java.util.List;
71  import java.util.Map;
72  import java.util.Properties;
73  import java.util.regex.Pattern;
74  
75  /**
76   * Miscellaneous Utility Methods
77   *
78   * @author Kuali Rice Team (rice.collab@kuali.org)
79   */
80  public final class KRADUtils {
81      private static KualiModuleService kualiModuleService;
82  
83      private static final KualiDecimal ONE_HUNDRED = new KualiDecimal("100.00");
84  
85      /**
86       * Prevent instantiation of the class.
87       */
88      private KRADUtils() {
89          throw new UnsupportedOperationException("do not call");
90      }
91  
92      /**
93       * Retrieve the title for a business object class
94       *
95       * <p>
96       * The title is a nicely formatted version of the simple class name.
97       * </p>
98       *
99       * @param clazz business object class
100      * @return title of the business object class
101      */
102     public final static String getBusinessTitleForClass(Class<? extends Object> clazz) {
103         if (clazz == null) {
104             throw new IllegalArgumentException(
105                     "The getBusinessTitleForClass method of KRADUtils requires a non-null class");
106         }
107         String className = clazz.getSimpleName();
108 
109         StringBuffer label = new StringBuffer(className.substring(0, 1));
110         for (int i = 1; i < className.length(); i++) {
111             if (Character.isLowerCase(className.charAt(i))) {
112                 label.append(className.charAt(i));
113             } else {
114                 label.append(" ").append(className.charAt(i));
115             }
116         }
117         return label.toString().trim();
118     }
119 
120     /**
121      * Picks off the filename from the full path
122      *
123      * <p>
124      * The different OS path separators are being taken into consideration.
125      * </p>
126      *
127      * @param fullFileNames file name with path
128      * @return file name
129      */
130     public final static List<String> getFileNameFromPath(List<String> fullFileNames) {
131         List<String> fileNameList = new ArrayList<String>();
132 
133         for (String fullFileName : fullFileNames) {
134             if (StringUtils.contains(fullFileName, "/")) {
135                 fileNameList.add(StringUtils.substringAfterLast(fullFileName, "/"));
136             } else {
137                 fileNameList.add(StringUtils.substringAfterLast(fullFileName, "\\"));
138             }
139         }
140 
141         return fileNameList;
142     }
143 
144     /**
145      * Convert the given money amount into an integer string.
146      *
147      * <p>
148      * Since the return string cannot have decimal point, multiplies the amount by 100 so the decimal places
149      * are not lost, for example, 320.15 is converted into 32015.
150      * </p>
151      *
152      * @param decimalNumber decimal number to be converted
153      * @return an integer string of the given money amount through multiplying by 100 and removing the fraction
154      *         portion.
155      */
156     public final static String convertDecimalIntoInteger(KualiDecimal decimalNumber) {
157         KualiDecimal decimalAmount = decimalNumber.multiply(ONE_HUNDRED);
158         NumberFormat formatter = NumberFormat.getIntegerInstance();
159         String formattedAmount = formatter.format(decimalAmount);
160 
161         return StringUtils.replace(formattedAmount, ",", "");
162     }
163 
164     /**
165      * Return the integer value of a string
166      *
167      * <p>
168      * If the string contains a decimal value everything after the decimal point is dropped.
169      * </p>
170      *
171      * @param numberStr string
172      * @return integer representation of the given string
173      */
174     public static Integer getIntegerValue(String numberStr) {
175         Integer numberInt = null;
176         try {
177             numberInt = new Integer(numberStr);
178         } catch (NumberFormatException nfe) {
179             Double numberDbl = new Double(numberStr);
180             numberInt = new Integer(numberDbl.intValue());
181         }
182         return numberInt;
183     }
184 
185     /**
186      * Attempt to coerce a String attribute value to the given propertyType.  If the transformation can't be made,
187      * either because the propertyType is null or because the transformation required exceeds this method's very small
188      * bag of tricks, then null is returned.
189      *
190      * @param propertyType the Class to coerce the attributeValue to
191      * @param attributeValue the String value to coerce
192      * @return an instance of the propertyType class, or null the transformation can't be made.
193      */
194     public static Object hydrateAttributeValue(Class<?> propertyType, String attributeValue) {
195         Object attributeValueObject = null;
196         if (propertyType != null && attributeValue != null) {
197             if (String.class.equals(propertyType)) {
198                 // it's already a String
199                 attributeValueObject = attributeValue;
200             } // KULRICE-6808: Kim Role Maintenance - Custom boolean role qualifier values are not being converted properly
201             else if (Boolean.class.equals(propertyType) || Boolean.TYPE.equals(propertyType)) {
202                 attributeValueObject = Truth.strToBooleanIgnoreCase(attributeValue);
203             } else {
204                 // try to create one with KRADUtils for other misc data types
205                 attributeValueObject = KRADUtils.createObject(propertyType, new Class[]{String.class},
206                         new Object[]{attributeValue});
207                 // if that didn't work, we'll get a null back
208             }
209         }
210         return attributeValueObject;
211     }
212 
213     public static Object createObject(Class<?> clazz, Class<?>[] argumentClasses, Object[] argumentValues) {
214         if (clazz == null) {
215             return null;
216         }
217         if (argumentClasses.length == 1 && argumentClasses[0] == String.class) {
218             if (argumentValues.length == 1 && argumentValues[0] != null) {
219                 if (clazz == String.class) {
220                     // this means we're trying to create a String from a String
221                     // don't new up Strings, it's a bad idea
222                     return argumentValues[0];
223                 } else {
224                     // maybe it's a type that supports valueOf?
225                     Method valueOfMethod = null;
226                     try {
227                         valueOfMethod = clazz.getMethod("valueOf", String.class);
228                     } catch (NoSuchMethodException e) {
229                         // ignored
230                     }
231                     if (valueOfMethod != null) {
232                         try {
233                             return valueOfMethod.invoke(null, argumentValues[0]);
234                         } catch (Exception e) {
235                             // ignored
236                         }
237                     }
238                 }
239             }
240         }
241         try {
242             Constructor<?> constructor = clazz.getConstructor(argumentClasses);
243             return constructor.newInstance(argumentValues);
244         } catch (Exception e) {
245             // ignored
246         }
247         return null;
248     }
249 
250     /**
251      * Creates a comma separated String representation of the given list.
252      *
253      * <p>
254      * For example 'a','b',c'.
255      * </p>
256      *
257      * @param list
258      * @return the joined String, empty if the list is null or has no elements
259      */
260     public static String joinWithQuotes(List<String> list) {
261         if (list == null || list.size() == 0) {
262             return "";
263         }
264 
265         return KRADConstants.SINGLE_QUOTE +
266                 StringUtils.join(list.iterator(), KRADConstants.SINGLE_QUOTE + "," + KRADConstants.SINGLE_QUOTE) +
267                 KRADConstants.SINGLE_QUOTE;
268     }
269 
270     private static KualiModuleService getKualiModuleService() {
271         if (kualiModuleService == null) {
272             kualiModuleService = KRADServiceLocatorWeb.getKualiModuleService();
273         }
274         return kualiModuleService;
275     }
276 
277     /**
278      * TODO this method will probably need to be exposed in a public KRADUtils class as it is used
279      * by several different modules.  That will have to wait until ModuleService and KualiModuleService are moved
280      * to core though.
281      */
282     public static String getNamespaceCode(Class<? extends Object> clazz) {
283         ModuleService moduleService = getKualiModuleService().getResponsibleModuleService(clazz);
284         if (moduleService == null) {
285             return KRADConstants.DEFAULT_NAMESPACE;
286         }
287         return moduleService.getModuleConfiguration().getNamespaceCode();
288     }
289 
290     public static Map<String, String> getNamespaceAndComponentSimpleName(Class<? extends Object> clazz) {
291         Map<String, String> map = new HashMap<String, String>();
292         map.put(KRADConstants.NAMESPACE_CODE, getNamespaceCode(clazz));
293         map.put(KRADConstants.COMPONENT_NAME, getComponentSimpleName(clazz));
294         return map;
295     }
296 
297     public static Map<String, String> getNamespaceAndComponentFullName(Class<? extends Object> clazz) {
298         Map<String, String> map = new HashMap<String, String>();
299         map.put(KRADConstants.NAMESPACE_CODE, getNamespaceCode(clazz));
300         map.put(KRADConstants.COMPONENT_NAME, getComponentFullName(clazz));
301         return map;
302     }
303 
304     public static Map<String, String> getNamespaceAndActionClass(Class<? extends Object> clazz) {
305         Map<String, String> map = new HashMap<String, String>();
306         map.put(KRADConstants.NAMESPACE_CODE, getNamespaceCode(clazz));
307         map.put(KRADConstants.ACTION_CLASS, clazz.getName());
308         return map;
309     }
310 
311     private static String getComponentSimpleName(Class<? extends Object> clazz) {
312         return clazz.getSimpleName();
313     }
314 
315     private static String getComponentFullName(Class<? extends Object> clazz) {
316         return clazz.getName();
317     }
318 
319     /**
320      * Parses a string that is in map format (commas separating map entries, colon separates
321      * map key/value) to a new map instance
322      *
323      * @param parameter - string parameter to parse
324      * @return Map<String, String> instance populated from string parameter
325      */
326     public static Map<String, String> convertStringParameterToMap(String parameter) {
327         Map<String, String> map = new HashMap<String, String>();
328 
329         if (StringUtils.isNotBlank(parameter)) {
330             if (StringUtils.contains(parameter, ",")) {
331                 String[] fieldConversions = StringUtils.split(parameter, ",");
332 
333                 for (int i = 0; i < fieldConversions.length; i++) {
334                     String fieldConversionStr = fieldConversions[i];
335                     if (StringUtils.isNotBlank(fieldConversionStr)) {
336                         if (StringUtils.contains(fieldConversionStr, ":")) {
337                             String[] fieldConversion = StringUtils.split(fieldConversionStr, ":");
338                             map.put(fieldConversion[0], fieldConversion[1]);
339                         } else {
340                             map.put(fieldConversionStr, fieldConversionStr);
341                         }
342                     }
343                 }
344             } else if (StringUtils.contains(parameter, ":")) {
345                 String[] fieldConversion = StringUtils.split(parameter, ":");
346                 map.put(fieldConversion[0], fieldConversion[1]);
347             } else {
348                 map.put(parameter, parameter);
349             }
350         }
351 
352         return map;
353     }
354 
355     /**
356      * Parses a string that is in list format (commas separating list entries) to a new List instance
357      *
358      * @param parameter - string parameter to parse
359      * @return List<String> instance populated from string parameter
360      */
361     public static List<String> convertStringParameterToList(String parameter) {
362         List<String> list = new ArrayList<String>();
363 
364         if (StringUtils.isNotBlank(parameter)) {
365             if (StringUtils.contains(parameter, ",")) {
366                 String[] parameters = StringUtils.split(parameter, ",");
367                 List arraysList = Arrays.asList(parameters);
368                 list.addAll(arraysList);
369             } else {
370                 list.add(parameter);
371             }
372         }
373 
374         return list;
375     }
376 
377     /**
378      * Translates characters in the given string like brackets that will cause
379      * problems with binding to characters that do not affect the binding
380      *
381      * @param key - string to translate
382      * @return String translated string
383      */
384     public static String translateToMapSafeKey(String key) {
385         String safeKey = key;
386 
387         safeKey = StringUtils.replace(safeKey, "[", "_");
388         safeKey = StringUtils.replace(safeKey, "]", "_");
389 
390         return safeKey;
391     }
392 
393     /**
394      * Builds a string from the given map by joining each entry with a comma and
395      * each key/value pair with a colon
396      *
397      * @param map - map instance to build string for
398      * @return String of map entries
399      */
400     public static String buildMapParameterString(Map<String, String> map) {
401         String parameterString = "";
402 
403         for (Map.Entry<String, String> entry : map.entrySet()) {
404             if (StringUtils.isNotBlank(parameterString)) {
405                 parameterString += ",";
406             }
407 
408             parameterString += entry.getKey() + ":" + entry.getValue();
409         }
410 
411         return parameterString;
412     }
413 
414     /**
415      * Parses the given string into a Map by splitting on the comma to get the
416      * map entries and within each entry splitting by colon to get the key/value
417      * pairs
418      *
419      * @param parameterString - string to parse into map
420      * @return Map<String, String> map from string
421      */
422     public static Map<String, String> getMapFromParameterString(String parameterString) {
423         Map<String, String> map = new HashMap<String, String>();
424 
425         String[] entries = parameterString.split(",");
426         for (int i = 0; i < entries.length; i++) {
427             String[] keyValue = entries[i].split(":");
428             if (keyValue.length != 2) {
429                 throw new RuntimeException("malformed field conversion pair: " + Arrays.toString(keyValue));
430             }
431 
432             map.put(keyValue[0], keyValue[1]);
433         }
434 
435         return map;
436     }
437 
438     /**
439      * Retrieves value for the given parameter name in the request and attempts to convert to a Boolean using
440      * the <code>BooleanFormatter</code>
441      *
442      * @param request - servlet request containing parameters
443      * @param parameterName - name of parameter to retrieve value for
444      * @return Boolean set to value of parameter, or null if parameter was not found in request
445      */
446     public static Boolean getRequestParameterAsBoolean(ServletRequest request, String parameterName) {
447         Boolean parameterValue = null;
448 
449         String parameterValueStr = request.getParameter(parameterName);
450         if (StringUtils.isNotBlank(parameterValueStr)) {
451             parameterValue = (Boolean) new BooleanFormatter().convertFromPresentationFormat(parameterValueStr);
452         }
453 
454         return parameterValue;
455     }
456 
457     /**
458      * Translates the given Map of String keys and String array values to a Map
459      * of String key and values. If the String array contains more than one
460      * value, the single string is built by joining the values with the vertical
461      * bar character
462      *
463      * @param requestParameters - Map of request parameters to translate
464      * @return Map<String, String> translated Map
465      */
466     public static Map<String, String> translateRequestParameterMap(Map<String, String[]> requestParameters) {
467         Map<String, String> parameters = new HashMap<String, String>();
468 
469         for (Map.Entry<String, String[]> parameter : requestParameters.entrySet()) {
470             String parameterValue = "";
471             if (parameter.getValue().length > 1) {
472                 parameterValue = StringUtils.join(parameter.getValue(), "|");
473             } else {
474                 parameterValue = parameter.getValue()[0];
475             }
476             parameters.put(parameter.getKey(), parameterValue);
477         }
478 
479         return parameters;
480     }
481 
482     /**
483      * Retrieves parameter values from the request that match the requested
484      * names. In addition, based on the object class an authorization check is
485      * performed to determine if the values are secure and should be decrypted.
486      * If true, the value is decrypted before returning
487      *
488      * @param parameterNames - names of the parameters whose values should be retrieved
489      * from the request
490      * @param parentObjectClass - object class that contains the parameter names as properties
491      * and should be consulted for security checks
492      * @param requestParameters - all request parameters to pull from
493      * @return Map<String, String> populated with parameter name/value pairs
494      *         pulled from the request
495      */
496     public static Map<String, String> getParametersFromRequest(List<String> parameterNames, Class<?> parentObjectClass,
497             Map<String, String> requestParameters) {
498         Map<String, String> parameterValues = new HashMap<String, String>();
499 
500         for (Iterator<String> iter = parameterNames.iterator(); iter.hasNext(); ) {
501             String keyPropertyName = iter.next();
502 
503             if (requestParameters.get(keyPropertyName) != null) {
504                 String keyValue = requestParameters.get(keyPropertyName);
505 
506                 // Check if this element was encrypted, if it was decrypt it
507                 if (KRADServiceLocatorWeb.getDataObjectAuthorizationService()
508                         .attributeValueNeedsToBeEncryptedOnFormsAndLinks(parentObjectClass, keyPropertyName)) {
509                     try {
510                         keyValue = StringUtils.removeEnd(keyValue, EncryptionService.ENCRYPTION_POST_PREFIX);
511                         keyValue = CoreApiServiceLocator.getEncryptionService().decrypt(keyValue);
512                     } catch (GeneralSecurityException e) {
513                         throw new RuntimeException(e);
514                     }
515                 }
516 
517                 parameterValues.put(keyPropertyName, keyValue);
518             }
519         }
520 
521         return parameterValues;
522     }
523 
524     /**
525      * Builds a Map containing a key/value pair for each property given in the property names list, general
526      * security is checked to determine if the value needs to be encrypted along with applying formatting to
527      * the value
528      *
529      * @param propertyNames - list of property names to get key/value pairs for
530      * @param dataObject - object instance containing the properties for which the values will be pulled
531      * @return Map<String, String> containing entry for each property name with the property name as the map key
532      *         and the property value as the value
533      */
534     public static Map<String, String> getPropertyKeyValuesFromDataObject(List<String> propertyNames,
535             Object dataObject) {
536         return getPropertyKeyValuesFromDataObject(propertyNames, Collections.<String>emptyList(), dataObject);
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 securePropertyNames - list of secure property names to match for encryption
546      * @param dataObject - object instance containing the properties for which the values will be pulled
547      * @return Map<String, String> containing entry for each property name with the property name as the map key
548      * and the property value as the value
549      */
550     public static Map<String, String> getPropertyKeyValuesFromDataObject(List<String> propertyNames,
551             List<String> securePropertyNames, Object dataObject) {
552         Map<String, String> propertyKeyValues = new HashMap<String, String>();
553 
554         if (dataObject == null) {
555             return propertyKeyValues;
556         }
557 
558         // iterate through properties and add a map entry for each
559         for (String propertyName : propertyNames) {
560             Object propertyValue = ObjectPropertyUtils.getPropertyValue(dataObject, propertyName);
561             if (propertyValue == null) {
562                 propertyValue = StringUtils.EMPTY;
563             }
564 
565             // secure values are not returned
566             if (!isSecure(propertyName, securePropertyNames, dataObject, propertyValue)) {
567                 propertyKeyValues.put(propertyName, propertyValue.toString());
568             }
569 
570         }
571 
572         return propertyKeyValues;
573     }
574 
575     /**
576      * Determines whether a property name should be secured, either based on installed sensitive data patterns, a list
577      * of secure property name patterns, or attributes in the Data Dictionary.
578      *
579      * @param propertyName The property name to check for security
580      * @param securePropertyNames The secure property name patterns to check
581      * @param dataObject The object containing this property
582      * @param propertyValue The value of the property
583      * @return true if the property needs to be secure, false otherwise
584      */
585     private static boolean isSecure(String propertyName, List<String> securePropertyNames, Object dataObject, Object propertyValue) {
586         if (propertyValue instanceof String && containsSensitiveDataPatternMatch((String) propertyValue)) {
587             return true;
588         }
589 
590         if (containsSecurePropertyName(propertyName, securePropertyNames)) {
591             return true;
592         }
593 
594         return KRADServiceLocatorWeb.getDataObjectAuthorizationService()
595                 .attributeValueNeedsToBeEncryptedOnFormsAndLinks(dataObject.getClass(), propertyName);
596     }
597 
598     /**
599      * Helper method to identify if propertyName contains a secure property name element.
600      * Check handles simple or compound names and ignores partial matches.
601      *
602      * @param propertyName property name as a single term or compound term (i.e. items[0].propertyName)
603      * @param securePropertyNames list of secure property names to match
604      * @return true if any of the secure property names are found in the property name, false otherwise
605      */
606     private static boolean containsSecurePropertyName(String propertyName, List<String> securePropertyNames) {
607         if (securePropertyNames == null) {
608             return false;
609         }
610 
611         for (String securePropertyName : securePropertyNames) {
612             // pattern prefix and suffix used to handle compound names and ignore partial name matches
613             if (Pattern.compile("(?:\\.|^)" + Pattern.quote(securePropertyName) + "(?:\\.|\\[|$)").matcher(propertyName)
614                     .find()) {
615                 return true;
616             }
617         }
618 
619         return false;
620     }
621 
622     /**
623      * Utility method to convert a Map to a Properties object
624      *
625      * @param parameters - map to convert
626      * @return Properties object containing all the map entries
627      */
628     public static Properties convertMapToProperties(Map<String, String> parameters) {
629         Properties properties = new Properties();
630 
631         if (parameters != null) {
632             for (Map.Entry<String, String> parameter : parameters.entrySet()) {
633                 properties.put(parameter.getKey(), parameter.getValue());
634             }
635         }
636 
637         return properties;
638     }
639 
640     /**
641      * Utility method to convert a Request Parameters Map to a Properties object
642      *
643      * <p>
644      * Multiple values for a parameter are joined together with comma delimiter
645      * </p>
646      *
647      * @param requestParameters - map to convert
648      * @return Properties object containing all the map entries
649      */
650     public static Properties convertRequestMapToProperties(Map<String, String[]> requestParameters) {
651         Properties properties = new Properties();
652 
653         if (requestParameters != null) {
654             for (Map.Entry<String, String[]> parameter : requestParameters.entrySet()) {
655                 String[] parameterValue = parameter.getValue();
656                 String parameterValueString = StringUtils.join(parameterValue, ",");
657 
658                 properties.put(parameter.getKey(), parameterValueString);
659             }
660         }
661 
662         return properties;
663     }
664 
665     /**
666      * Check if data might be sensitive
667      *
668      * <p>
669      * The sensitivity of the data is checked by matching it against the sensitive data patterns that are specified
670      * in the system parameter table.
671      * </p>
672      *
673      * @param fieldValue data to be checked for sensitivity
674      * @return true if the data matches the sensitive data pattern, false otherwise
675      */
676     public static boolean containsSensitiveDataPatternMatch(String fieldValue) {
677         if (StringUtils.isBlank(fieldValue)) {
678             return false;
679         }
680 
681         ParameterService parameterService = CoreFrameworkServiceLocator.getParameterService();
682         Collection<String> sensitiveDataPatterns = parameterService.getParameterValuesAsString(
683                 KRADConstants.KNS_NAMESPACE, ParameterConstants.ALL_COMPONENT,
684                 KRADConstants.SystemGroupParameterNames.SENSITIVE_DATA_PATTERNS);
685 
686         for (String pattern : sensitiveDataPatterns) {
687             if (Pattern.compile(pattern).matcher(fieldValue).find()) {
688                 return true;
689             }
690         }
691 
692         return false;
693     }
694 
695     /**
696      * Strips out common patterns used in cross side scripting
697      *
698      * @param value string to strip patterns from
699      * @return cleaned string
700      */
701     public static String stripXSSPatterns(String value) {
702         if (value == null) {
703             return null;
704         }
705 
706         // Avoid null characters
707         value = value.replaceAll("", "");
708 
709         // Avoid anything between script tags
710         Pattern scriptPattern = Pattern.compile("<script>(.*?)</script>", Pattern.CASE_INSENSITIVE);
711         value = scriptPattern.matcher(value).replaceAll("");
712 
713         // Avoid anything in a src='...' type of expression
714         scriptPattern = Pattern.compile("src[\r\n]*=[\r\n]*\\\'(.*?)\\\'",
715                 Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
716         value = scriptPattern.matcher(value).replaceAll("");
717 
718         scriptPattern = Pattern.compile("src[\r\n]*=[\r\n]*\\\"(.*?)\\\"",
719                 Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
720         value = scriptPattern.matcher(value).replaceAll("");
721 
722         // Remove any lonesome </script> tag
723         scriptPattern = Pattern.compile("</script>", Pattern.CASE_INSENSITIVE);
724         value = scriptPattern.matcher(value).replaceAll("");
725 
726         // Remove any lonesome <script ...> tag
727         scriptPattern = Pattern.compile("<script(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
728         value = scriptPattern.matcher(value).replaceAll("");
729 
730         // Avoid eval(...) expressions
731         scriptPattern = Pattern.compile("eval\\((.*?)\\)",
732                 Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
733         value = scriptPattern.matcher(value).replaceAll("");
734 
735         // Avoid expression(...) expressions
736         scriptPattern = Pattern.compile("expression\\((.*?)\\)",
737                 Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
738         value = scriptPattern.matcher(value).replaceAll("");
739 
740         // Avoid javascript:... expressions
741         scriptPattern = Pattern.compile("javascript:", Pattern.CASE_INSENSITIVE);
742         value = scriptPattern.matcher(value).replaceAll("");
743 
744         // Avoid vbscript:... expressions
745         scriptPattern = Pattern.compile("vbscript:", Pattern.CASE_INSENSITIVE);
746         value = scriptPattern.matcher(value).replaceAll("");
747 
748         // Avoid onload= expressions
749         scriptPattern = Pattern.compile("onload(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
750         value = scriptPattern.matcher(value).replaceAll("");
751 
752         return value;
753     }
754 
755     /**
756      * Gets the UserSession object from the HttpServletRequest object's
757      * associated session.
758      *
759      * <p>
760      * In some cases (different threads) the UserSession cannot be retrieved
761      * from GlobalVariables but can still be accessed via the session object
762      * </p>
763      */
764     public static final UserSession getUserSessionFromRequest(HttpServletRequest request) {
765         return (UserSession) request.getSession().getAttribute(KRADConstants.USER_SESSION_KEY);
766     }
767 
768     /**
769      * Check if current deployment is the production environment
770      *
771      * @return true if the deploy environment is production, false otherwise
772      */
773     public static boolean isProductionEnvironment() {
774         return CoreApiServiceLocator.getKualiConfigurationService().getPropertyValueAsString(
775                 KRADConstants.PROD_ENVIRONMENT_CODE_KEY).equalsIgnoreCase(
776                 CoreApiServiceLocator.getKualiConfigurationService().getPropertyValueAsString(
777                         KRADConstants.ENVIRONMENT_KEY));
778     }
779 
780     /**
781      * Gets the message associated with ErrorMessage object passed in, using message service.
782      * The prefix and suffix will be appended to the retrieved message if processPrefixSuffix is true and if those
783      * settings are set on the ErrorMessage passed in.
784      *
785      * @param errorMessage the ErrorMessage object containing the message key(s)
786      * @param processPrefixSuffix if true appends the prefix and suffix to the message if they exist on ErrorMessage
787      * @return the converted/retrieved message
788      */
789     public static String getMessageText(ErrorMessage errorMessage, boolean processPrefixSuffix) {
790         String message = "";
791         if (errorMessage != null && errorMessage.getErrorKey() != null) {
792             MessageService messageService = KRADServiceLocatorWeb.getMessageService();
793 
794             // find message by key
795             message = messageService.getMessageText(errorMessage.getNamespaceCode(), errorMessage.getComponentCode(),
796                     errorMessage.getErrorKey());
797             if (message == null) {
798                 message = "Intended message with key: " + errorMessage.getErrorKey() + " not found.";
799             }
800 
801             if (errorMessage.getMessageParameters() != null && StringUtils.isNotBlank(message)) {
802                 message = message.replace("'", "''");
803                 message = MessageFormat.format(message, (Object[]) errorMessage.getMessageParameters());
804             }
805 
806             // add prefix
807             if (StringUtils.isNotBlank(errorMessage.getMessagePrefixKey()) && processPrefixSuffix) {
808                 String prefix = messageService.getMessageText(errorMessage.getNamespaceCode(),
809                         errorMessage.getComponentCode(), errorMessage.getMessagePrefixKey());
810 
811                 if (errorMessage.getMessagePrefixParameters() != null && StringUtils.isNotBlank(prefix)) {
812                     prefix = prefix.replace("'", "''");
813                     prefix = MessageFormat.format(prefix, (Object[]) errorMessage.getMessagePrefixParameters());
814                 }
815 
816                 if (StringUtils.isNotBlank(prefix)) {
817                     message = prefix + " " + message;
818                 }
819             }
820 
821             // add suffix
822             if (StringUtils.isNotBlank(errorMessage.getMessageSuffixKey()) && processPrefixSuffix) {
823                 String suffix = messageService.getMessageText(errorMessage.getNamespaceCode(),
824                         errorMessage.getComponentCode(), errorMessage.getMessageSuffixKey());
825 
826                 if (errorMessage.getMessageSuffixParameters() != null && StringUtils.isNotBlank(suffix)) {
827                     suffix = suffix.replace("'", "''");
828                     suffix = MessageFormat.format(suffix, (Object[]) errorMessage.getMessageSuffixParameters());
829                 }
830 
831                 if (StringUtils.isNotBlank(suffix)) {
832                     message = message + " " + suffix;
833                 }
834             }
835         }
836 
837         return message;
838     }
839 
840     /**
841      * Generate the request parameter portion of the url based on the map of key value pairs passed in
842      *
843      * @param requestParameters the request parameters to use in the string
844      * @return a request parameter string starting with "?" with "&" separators, or blank if the mapped passed in is
845      *         blank
846      */
847     public static String getRequestStringFromMap(Map<String, String> requestParameters) {
848         String requestString = "";
849 
850         if (requestParameters.isEmpty()) {
851             return requestString;
852         }
853 
854         URLCodec urlCodec = new URLCodec(KRADConstants.DEFAULT_ENCODING);
855 
856         for (String key : requestParameters.keySet()) {
857             String value = null;
858             try {
859                 value = urlCodec.encode(requestParameters.get(key));
860             } catch (EncoderException e) {
861                 throw new RuntimeException("Unable to encode parameter name or value: " + key + "=" + value, e);
862             }
863 
864             if (StringUtils.isNotBlank(requestString)) {
865                 requestString = requestString + "&";
866             }
867 
868             requestString = requestString + key + "=" + value;
869         }
870 
871         return "?" + requestString;
872     }
873 
874     /**
875      * Helper method for building a URL that will invoke the given controller and render the given
876      * KRAD view
877      *
878      * @param baseUrl base url (domain, port)
879      * @param controllerMapping mapping for the controller that should be invoked
880      * @param viewId id for the view that should be rendered
881      * @return url for invoking the view
882      */
883     public static String buildViewUrl(String baseUrl, String controllerMapping, String viewId) {
884         Assert.hasLength(baseUrl, "base url is null or empty");
885         Assert.hasLength(controllerMapping, "controller mapping is null or empty");
886         Assert.hasLength(viewId, "view id is null or empty");
887 
888         StringBuffer url = new StringBuffer();
889 
890         url.append(baseUrl);
891 
892         if (!baseUrl.endsWith("/")) {
893             url.append("/");
894         }
895 
896         url.append(controllerMapping);
897 
898         url.append("?");
899         url.append(UifParameters.VIEW_ID);
900         url.append("=");
901         url.append(viewId);
902 
903         return url.toString();
904     }
905 
906     /**
907      * Removes parameters from the given properties object that are request specific (useful when manupulating the
908      * current URL to invoke something else)
909      *
910      * @param requestParameters properties instance containing the parameters to clean
911      */
912     public static void cleanRequestParameters(Properties requestParameters) {
913         requestParameters.remove(UifParameters.SESSION_ID);
914         requestParameters.remove(UifParameters.AJAX_REQUEST);
915         requestParameters.remove(UifParameters.AJAX_RETURN_TYPE);
916         requestParameters.remove(UifParameters.FORM_KEY);
917         requestParameters.remove(UifParameters.JUMP_TO_ID);
918         requestParameters.remove(UifParameters.FOCUS_ID);
919     }
920 
921     /**
922      * Get the full url for a request (requestURL + queryString)
923      *
924      * @param request the request
925      * @return the fullUrl
926      */
927     public static String getFullURL(HttpServletRequest request) {
928         if (request == null) {
929             return null;
930         }
931 
932         StringBuffer requestURL = request.getRequestURL();
933         String queryString = request.getQueryString();
934 
935         if (queryString == null) {
936             return requestURL.toString();
937         } else {
938             return requestURL.append('?').append(queryString).toString();
939         }
940     }
941 
942     /**
943      * Attempts to generate a unique view title by combining the View's headerText with the title attribute for the
944      * dataObjectClass found through the DataObjectMetaDataService.  If the title attribute cannot be found, just the
945      * headerText is returned.
946      *
947      * @param form the form
948      * @param view the view
949      * @return the headerText with the title attribute in parenthesis or just the headerText if it title attribute
950      *         cannot be determined
951      */
952     public static String generateUniqueViewTitle(UifFormBase form, View view) {
953         String title = view.getHeader().getHeaderText();
954 
955         String viewLabelPropertyName = "";
956 
957         Class<?> dataObjectClass;
958         if (StringUtils.isNotBlank(view.getDefaultBindingObjectPath())) {
959             dataObjectClass = ObjectPropertyUtils.getPropertyType(form, view.getDefaultBindingObjectPath());
960         } else {
961             dataObjectClass = view.getFormClass();
962         }
963 
964         DataObjectMetaDataService mds = KRADServiceLocatorWeb.getDataObjectMetaDataService();
965         if (dataObjectClass != null) {
966             viewLabelPropertyName = mds.getTitleAttribute(dataObjectClass);
967         }
968 
969         String viewLabelPropertyPath = "";
970         if (StringUtils.isNotBlank(viewLabelPropertyName)) {
971             // adjust binding prefix
972             if (!viewLabelPropertyName.startsWith(UifConstants.NO_BIND_ADJUST_PREFIX)) {
973                 if (StringUtils.isNotBlank(view.getDefaultBindingObjectPath())) {
974                     viewLabelPropertyPath = view.getDefaultBindingObjectPath() + "." + viewLabelPropertyName;
975                 }
976             } else {
977                 viewLabelPropertyPath = StringUtils.removeStart(viewLabelPropertyName,
978                         UifConstants.NO_BIND_ADJUST_PREFIX);
979             }
980         } else {
981             // attempt to get title attribute
982             if (StringUtils.isNotBlank(view.getDefaultBindingObjectPath())) {
983                 dataObjectClass = ViewModelUtils.getObjectClassForMetadata(view, form,
984                         view.getDefaultBindingObjectPath());
985             } else {
986                 dataObjectClass = view.getFormClass();
987             }
988 
989             if (dataObjectClass != null) {
990                 String titleAttribute = mds.getTitleAttribute(dataObjectClass);
991                 if (StringUtils.isNotBlank(titleAttribute)) {
992                     viewLabelPropertyPath = view.getDefaultBindingObjectPath() + "." + titleAttribute;
993                 }
994             }
995         }
996 
997         Object viewLabelPropertyValue = null;
998         if (StringUtils.isNotBlank(viewLabelPropertyPath) && ObjectPropertyUtils.isReadableProperty(form,
999                 viewLabelPropertyPath)) {
1000             viewLabelPropertyValue = ObjectPropertyUtils.getPropertyValue(form, viewLabelPropertyPath);
1001         }
1002 
1003         if (viewLabelPropertyValue != null && StringUtils.isNotBlank(viewLabelPropertyValue.toString()) && StringUtils
1004                 .isNotBlank(title)) {
1005             return title + " (" + viewLabelPropertyValue.toString() + ")";
1006         } else {
1007             return title;
1008         }
1009     }
1010 
1011     /**
1012      * Attempts to extract a string value out of the field passed in, varies depending on field type
1013      *
1014      * <p>If the field is a dataField, it will use its propertyName to retrieve a value, otherwise it will try to
1015      * retrieve textual content out of various component types.  If the field is a FieldGroup, only the first
1016      * component's determined value will be used.  This function is used for sorting.</p>
1017      *
1018      * @param model the current model
1019      * @param field the field to get a value from
1020      * @return the field's String value, false if it cant be determined
1021      */
1022     public static String getSimpleFieldValue(Object model, Field field) {
1023         if (field == null) {
1024             return null;
1025         }
1026 
1027         String value = null;
1028         // check for what type of field this is
1029         if (field instanceof DataField) {
1030             String propertyPath = ((DataField) field).getBindingInfo().getBindingPath();
1031             Object valueObject = null;
1032 
1033             if (field.isHidden()) {
1034                 return "";
1035             }
1036 
1037             // check if readable
1038             if (ObjectPropertyUtils.isReadableProperty(model, propertyPath)) {
1039                 valueObject = ObjectPropertyUtils.getPropertyValue(model, propertyPath);
1040             }
1041 
1042             // use object's string value
1043             if (valueObject != null && !((DataField) field).isApplyMask()) {
1044                 value = valueObject.toString();
1045             } else if (valueObject != null && ((DataField) field).isApplyMask()) {
1046                 value = ((DataField) field).getMaskFormatter().maskValue(valueObject);
1047             }
1048         } else if (field instanceof ActionField) {
1049             value = ((ActionField) field).getActionLabel();
1050 
1051             // use image alt text if any
1052             if (StringUtils.isBlank(value) && ((ActionField) field).getActionImage() != null) {
1053                 value = ((ActionField) field).getActionImage().getAltText();
1054             }
1055         } else if (field instanceof LinkField) {
1056             value = ((LinkField) field).getLinkText();
1057         } else if (field instanceof ImageField) {
1058             value = ((ImageField) field).getAltText();
1059         } else if (field instanceof MessageField && ((MessageField) field).getMessage() != null) {
1060             value = ((MessageField) field).getMessage().getMessageText();
1061         } else if (field instanceof SpaceField) {
1062             value = "";
1063         } else if (field instanceof FieldGroup
1064                 && ((FieldGroup) field).getGroup() != null
1065                 && ((FieldGroup) field).getGroup().getItems() != null
1066                 && !((FieldGroup) field).getGroup().getItems().isEmpty()) {
1067             // using first components type for assumed value
1068             Component firstComponent = ((FieldGroup) field).getGroup().getItems().get(0);
1069 
1070             // check first component type to extract value
1071             if (firstComponent != null && firstComponent instanceof Field) {
1072                 value = getSimpleFieldValue(model, field);
1073             } else if (firstComponent instanceof Action && StringUtils.isNotBlank(
1074                     ((Action) firstComponent).getActionLabel())) {
1075                 value = ((Action) firstComponent).getActionLabel();
1076             } else if (firstComponent instanceof Action && ((Action) firstComponent).getActionImage() != null) {
1077                 value = ((Action) firstComponent).getActionImage().getAltText();
1078             } else if (firstComponent instanceof Link) {
1079                 value = ((Link) firstComponent).getLinkText();
1080             } else if (firstComponent instanceof Image) {
1081                 value = ((Image) firstComponent).getAltText();
1082             } else if (firstComponent instanceof org.kuali.rice.krad.uif.element.Message) {
1083                 value = ((org.kuali.rice.krad.uif.element.Message) firstComponent).getMessageText();
1084             } else {
1085                 value = null;
1086             }
1087         }
1088 
1089         return value;
1090     }
1091 
1092     /**
1093      * Helper method to change common characters into HTML attribute safe characters
1094      *
1095      * @param message the string to convert
1096      * @return the converted string with quotes, sing quotes, and slash replaced
1097      */
1098     public static String convertToHTMLAttributeSafeString(String message) {
1099         if (StringUtils.isBlank(message)) {
1100             return message;
1101         }
1102 
1103         if (message.contains("\"")) {
1104             message = message.replace("\"", "&quot;");
1105         }
1106         if (message.contains("'")) {
1107             message = message.replace("'", "&#39;");
1108         }
1109         if (message.contains("\\")) {
1110             message = message.replace("\\", "&#92;");
1111         }
1112 
1113         return message;
1114     }
1115 
1116     /**
1117      * Get the rowCss for the line specified, by evaluating the conditionalRowCssClasses map for that row
1118      *
1119      * @param conditionalRowCssClasses the conditionalRowCssClass map, where key is the condition and value is
1120      * the class(es)
1121      * @param lineIndex the line/row index
1122      * @param isOdd true if the row is considered odd
1123      * @param lineContext the lineContext for expressions, pass null if not applicable
1124      * @param expressionEvaluator the expressionEvaluator, pass null if not applicable
1125      * @return row csss class String for the class attribute of this row
1126      */
1127     public static String generateRowCssClassString(Map<String, String> conditionalRowCssClasses, int lineIndex,
1128             boolean isOdd, Map<String, Object> lineContext, ExpressionEvaluator expressionEvaluator) {
1129         String rowCss = "";
1130         if (conditionalRowCssClasses == null || conditionalRowCssClasses.isEmpty()) {
1131             return rowCss;
1132         }
1133 
1134         for (String cssRule : conditionalRowCssClasses.keySet()) {
1135             if (cssRule.startsWith(UifConstants.EL_PLACEHOLDER_PREFIX) && lineContext != null &&
1136                     expressionEvaluator != null) {
1137                 String outcome = expressionEvaluator.evaluateExpressionTemplate(lineContext, cssRule);
1138                 if (outcome != null && Boolean.parseBoolean(outcome)) {
1139                     rowCss = rowCss + " " + conditionalRowCssClasses.get(cssRule);
1140                 }
1141             } else if (cssRule.equals(UifConstants.RowSelection.ALL)) {
1142                 rowCss = rowCss + " " + conditionalRowCssClasses.get(cssRule);
1143             } else if (cssRule.equals(UifConstants.RowSelection.EVEN) && !isOdd) {
1144                 rowCss = rowCss + " " + conditionalRowCssClasses.get(cssRule);
1145             } else if (cssRule.equals(UifConstants.RowSelection.ODD) && isOdd) {
1146                 rowCss = rowCss + " " + conditionalRowCssClasses.get(cssRule);
1147             } else if (StringUtils.isNumeric(cssRule) && (lineIndex + 1) == Integer.parseInt(cssRule)) {
1148                 rowCss = rowCss + " " + conditionalRowCssClasses.get(cssRule);
1149             }
1150         }
1151 
1152         rowCss = StringUtils.removeStart(rowCss, " ");
1153 
1154         return rowCss;
1155     }
1156 }