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