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