001    /**
002     * Copyright 2005-2014 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.opensource.org/licenses/ecl2.php
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.rice.krad.util;
017    
018    import org.apache.commons.codec.EncoderException;
019    import org.apache.commons.codec.net.URLCodec;
020    import org.apache.commons.lang.StringUtils;
021    import org.kuali.rice.core.api.CoreApiServiceLocator;
022    import org.kuali.rice.core.api.encryption.EncryptionService;
023    import org.kuali.rice.core.api.util.Truth;
024    import org.kuali.rice.core.api.util.type.KualiDecimal;
025    import org.kuali.rice.core.web.format.BooleanFormatter;
026    import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
027    import org.kuali.rice.coreservice.framework.parameter.ParameterConstants;
028    import org.kuali.rice.coreservice.framework.parameter.ParameterService;
029    import org.kuali.rice.krad.UserSession;
030    import org.kuali.rice.krad.messages.Message;
031    import org.kuali.rice.krad.messages.MessageService;
032    import org.kuali.rice.krad.service.DataObjectMetaDataService;
033    import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
034    import org.kuali.rice.krad.service.KualiModuleService;
035    import org.kuali.rice.krad.service.ModuleService;
036    import org.kuali.rice.krad.uif.UifConstants;
037    import org.kuali.rice.krad.uif.UifParameters;
038    import org.kuali.rice.krad.uif.component.Component;
039    import org.kuali.rice.krad.uif.element.Action;
040    import org.kuali.rice.krad.uif.element.Image;
041    import org.kuali.rice.krad.uif.element.Link;
042    import org.kuali.rice.krad.uif.field.ActionField;
043    import org.kuali.rice.krad.uif.field.DataField;
044    import org.kuali.rice.krad.uif.field.Field;
045    import org.kuali.rice.krad.uif.field.FieldGroup;
046    import org.kuali.rice.krad.uif.field.ImageField;
047    import org.kuali.rice.krad.uif.field.LinkField;
048    import org.kuali.rice.krad.uif.field.MessageField;
049    import org.kuali.rice.krad.uif.field.SpaceField;
050    import org.kuali.rice.krad.uif.util.ObjectPropertyUtils;
051    import org.kuali.rice.krad.uif.util.ViewModelUtils;
052    import org.kuali.rice.krad.uif.view.ExpressionEvaluator;
053    import org.kuali.rice.krad.uif.view.View;
054    import org.kuali.rice.krad.web.form.UifFormBase;
055    import org.springframework.util.Assert;
056    
057    import javax.servlet.ServletRequest;
058    import javax.servlet.http.HttpServletRequest;
059    import java.lang.reflect.Constructor;
060    import java.lang.reflect.Method;
061    import java.security.GeneralSecurityException;
062    import java.text.MessageFormat;
063    import java.text.NumberFormat;
064    import java.util.ArrayList;
065    import java.util.Arrays;
066    import java.util.Collection;
067    import java.util.Collections;
068    import java.util.HashMap;
069    import java.util.Iterator;
070    import java.util.List;
071    import java.util.Map;
072    import java.util.Properties;
073    import java.util.regex.Pattern;
074    
075    /**
076     * Miscellaneous Utility Methods
077     *
078     * @author Kuali Rice Team (rice.collab@kuali.org)
079     */
080    public final class KRADUtils {
081        private static KualiModuleService kualiModuleService;
082    
083        private static final KualiDecimal ONE_HUNDRED = new KualiDecimal("100.00");
084    
085        /**
086         * Prevent instantiation of the class.
087         */
088        private KRADUtils() {
089            throw new UnsupportedOperationException("do not call");
090        }
091    
092        /**
093         * Retrieve the title for a business object class
094         *
095         * <p>
096         * The title is a nicely formatted version of the simple class name.
097         * </p>
098         *
099         * @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    }