org.opentides.util.CrudUtil.java Source code

Java tutorial

Introduction

Here is the source code for org.opentides.util.CrudUtil.java

Source

/*
   Licensed to the Apache Software Foundation (ASF) under one
   or more contributor license agreements.  See the NOTICE file
   distributed with this work for additional information
   regarding copyright ownership.  The ASF licenses this file
   to you under the Apache License, Version 2.0 (the
   "License"); you may not use this file except in compliance
   with the License.  You may obtain a copy of the License at
    
 http://www.apache.org/licenses/LICENSE-2.0
    
   Unless required by applicable law or agreed to in writing,
   software distributed under the License is distributed on an
   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   KIND, either express or implied.  See the License for the
   specific language governing permissions and limitations
   under the License.    
 */

package org.opentides.util;

import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.log4j.Logger;
import org.hibernate.LazyInitializationException;
import org.opentides.annotation.Auditable;
import org.opentides.bean.AuditableField;
import org.opentides.bean.BaseEntity;
import org.opentides.bean.MessageResponse;
import org.opentides.bean.SystemCodes;
import org.opentides.exception.InvalidImplementationException;
import org.springframework.context.MessageSource;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.Assert;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;

/**
 * @author allanctan
 *
 */
public class CrudUtil {

    private static final Logger _log = Logger.getLogger(CrudUtil.class);

    private static final String SQL_PARAM = ":([^\\s]+)";
    private static final Pattern SQL_PARAM_PATTERN = Pattern.compile(SQL_PARAM,
            Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);

    private static final String URL_ENCODING = "UTF-8";

    /**
     * Hide the constructor.
     */
    private CrudUtil() {
    }

    /**
     * Creates the logging message for new audit logs 
     * @param obj
     * @return
     */
    public static String buildCreateMessage(BaseEntity obj) {
        StringBuilder message = new StringBuilder();
        if (obj.getClass().isAnnotationPresent(Auditable.class)) {
            AuditableField pf = CacheUtil.getPrimaryField(obj);
            message.append("<p class='add-message'>Added new ");
            message.append(buildPrimaryField(pf, obj)).append(" with the following: ");

            // loop through the fields list
            List<AuditableField> auditFields = CacheUtil.getAuditable(obj);
            int count = 0;
            for (AuditableField property : auditFields) {
                Object ret = retrieveNullableObjectValue(obj, property.getFieldName());
                ret = normalizeValue(ret);
                if (ret.toString().trim().length() > 0 && !pf.getFieldName().equals(property.getFieldName())) {
                    if (count > 0)
                        message.append("and ");
                    message.append(property.getTitle()).append("=<span class='field-value'>").append(ret.toString())
                            .append("</span> ");
                    count++;
                }
            }
            message.append("</p>");
        }
        return message.toString();
    }

    /**
     * Returns the list of fields that have difference values or updated.
     * 
     * @param oldObject
     * @param newObject
     * @return
     */
    public static List<String> getUpdatedFields(BaseEntity oldObject, BaseEntity newObject) {
        List<String> fields = CacheUtil.getPersistentFields(oldObject);
        List<String> updatedFields = new ArrayList<String>();
        for (String field : fields) {
            Object oldValue = retrieveNullableObjectValue(oldObject, field);
            Object newValue = retrieveNullableObjectValue(newObject, field);
            oldValue = normalizeValue(oldValue);
            newValue = normalizeValue(newValue);
            if (!oldValue.equals(newValue)) {
                updatedFields.add(field);
            }
        }
        return updatedFields;
    }

    /**
     * Creates the logging message for update audit logs 
     * @param obj
     * @return
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public static String buildUpdateMessage(BaseEntity oldObject, BaseEntity newObject) {

        StringBuilder message = new StringBuilder("<p class='change-message'>Changed ");
        AuditableField pf = CacheUtil.getPrimaryField(oldObject);
        message.append(buildPrimaryField(pf, oldObject)).append(" with the following: ");
        // loop through the fields list
        List<AuditableField> auditableFields = CacheUtil.getAuditable(oldObject);
        int count = 0;

        // scenarios
        // 1 - collection vs null -> Enter collection
        // 2 - collection vs collection -> Enter collection
        // 3 - object vs null -> Enter object
        // 4 - object vs object -> Enter object
        // 5 - collection vs object -> Invalid or convert collection to object. 

        for (AuditableField property : auditableFields) {
            _log.debug("Building update message for field " + property.getFieldName());
            Object oldValue = retrieveNullableObjectValue(oldObject, property.getFieldName());
            Object newValue = retrieveNullableObjectValue(newObject, property.getFieldName());
            oldValue = normalizeValue(oldValue);
            newValue = normalizeValue(newValue);

            if (oldValue.getClass() != newValue.getClass()) {
                _log.debug("Old object: " + oldValue);
                _log.warn("Unable to compare [" + property.getFieldName()
                        + "] for audit logging due to difference in datatype. " + "oldValue is ["
                        + oldValue.getClass() + "] and newValue is [" + newValue.getClass() + "]");
                continue;
            }

            if (Collection.class.isAssignableFrom(oldValue.getClass())
                    && Collection.class.isAssignableFrom(newValue.getClass())) {
                if (((Collection) oldValue).isEmpty() && ((Collection) newValue).isEmpty()) {
                    _log.debug("Old and New values are empty");
                    continue;
                }
                _log.debug("Old and New values are not empty");
                List addedList = new ArrayList((Collection) newValue);
                //Collection addedCollection = (Collection)newValue;
                //addedCollection.removeAll((Collection)oldValue);
                //addedList.addAll(new ArrayList((Collection)newValue));
                addedList.removeAll(new ArrayList((Collection) oldValue));

                List removedList = new ArrayList((Collection) oldValue);
                //removedList.addAll((List) oldValue);
                removedList.removeAll(new ArrayList((Collection) newValue));
                //Collection removedCollection = (Collection)oldValue;
                //removedCollection.remove((Collection)newValue);
                if (!addedList.isEmpty() || !removedList.isEmpty()) {
                    if (count > 0)
                        message.append("and ");
                }
                if (!addedList.isEmpty()) {
                    message.append("added ").append(property.getTitle())
                            .append(" <span class='field-values-added'>").append(addedList).append("</span> ");
                }
                if (!removedList.isEmpty()) {
                    if (!addedList.isEmpty())
                        message.append("and ");
                    message.append("removed ").append(property.getTitle())
                            .append(" <span class='field-values-removed'>").append(removedList).append("</span> ");
                    count++;
                }
            } else {
                if (!oldValue.equals(newValue)) {
                    if (count > 0)
                        message.append("and ");
                    if (StringUtil.isEmpty(newValue.toString())) {
                        message.append(property.getTitle()).append(" <span class='field-value-removed'>")
                                .append(oldValue.toString()).append("</span> is removed ");
                    } else {
                        message.append(property.getTitle());
                        if (!StringUtil.isEmpty(oldValue.toString()))
                            message.append(" from <span class='field-value-from'>").append(oldValue.toString())
                                    .append("</span> ");
                        else
                            message.append(" is set ");
                        message.append("to <span class='field-value-to'>").append(newValue.toString())
                                .append("</span> ");
                    }
                    count++;
                }
            }
        }
        message.append("</p>");
        if (count == 0)
            return "";
        else
            return message.toString();
    }

    /**
     * Creates the logging message for deleted records. 
     * 
     * @param obj
     * @return
     */
    public static String buildDeleteMessage(BaseEntity obj) {
        StringBuilder message = new StringBuilder("<p class='delete-message'>Deleted ");
        AuditableField pf = CacheUtil.getPrimaryField(obj);
        message.append(buildPrimaryField(pf, obj)).append("</p>");
        return message.toString();
    }

    /**
     * Private helper to build primary field message.
     * @param pf
     * @param obj
     * @return
     */
    private static String buildPrimaryField(AuditableField pf, BaseEntity obj) {
        StringBuilder message = new StringBuilder();
        String shortName = CacheUtil.getReadableName(obj);
        message.append(shortName); // class name
        Object value = retrieveNullableObjectValue(obj, pf.getFieldName());
        if (value != null && !StringUtil.isEmpty(value.toString()))
            message.append(" with ").append(pf.getTitle()).append(":<span class='primary-field'>")
                    .append(value.toString()).append("</span>");
        return message.toString();
    }

    /**
     * Private helper that converts object into other form for 
     * audit log comparison and display purposes.
     * @param obj
     * @return
     */
    private static Object normalizeValue(Object obj) {
        // convert date into string
        if (obj == null)
            return "";
        // if object is empty collection, convert to empty string?       
        if (obj instanceof Collection) {
            try {
                if (((Collection<?>) obj).isEmpty())
                    return "";
            } catch (LazyInitializationException lie) {
                //TODO: Need to check if this is safe
                return "";
            }
        }

        if (obj instanceof Date) {
            if (DateUtil.hasTime((Date) obj)) {
                return DateUtil.dateToString((Date) obj, "EEE, dd MMM yyyy HH:mm:ss z");
            } else
                return DateUtil.dateToString((Date) obj, "EEE, dd MMM yyyy");
        }
        return obj;
    }

    /**
     * Helper function that converts the given object into a map value.
     * 
     * @param object
     * @return
     */
    public static Map<String, Object> buildMapValues(BaseEntity object) {
        return buildMapValues(object, CacheUtil.getPersistentFields(object));
    }

    /**
     * Helper function that converts the given object into a map value.
     * 
     * @param object
     * @param fields
     * @return
     */
    @SuppressWarnings("rawtypes")
    public static Map<String, Object> buildMapValues(BaseEntity object, List<String> fields) {
        Map<String, Object> map = new HashMap<String, Object>();
        for (String property : fields) {
            // get the value
            Object ret = retrieveObjectValue(object, property);
            if (ret != null) {
                if (SystemCodes.class.isAssignableFrom(ret.getClass())) {
                    SystemCodes sc = (SystemCodes) ret;
                    if (!StringUtil.isEmpty(sc.getKey())) {
                        map.put(property, sc.getKey());
                    }
                } else if (BaseEntity.class.isAssignableFrom(ret.getClass())) {
                    BaseEntity be = (BaseEntity) ret;
                    if (be.getId() != null) {
                        map.put(property, be.getId());
                    }
                } else if (Collection.class.isAssignableFrom(ret.getClass())) {
                    Collection c = (Collection) ret;
                    Iterator itr = c.iterator();
                    while (itr.hasNext()) {
                        Object value = itr.next();
                        if (value != null) {
                            if (BaseEntity.class.isAssignableFrom(value.getClass())) {
                                BaseEntity be = (BaseEntity) value;
                                if (be.getId() != null) {
                                    map.put(property, be.getId());
                                }
                            } else
                                map.put(property, value);
                        }
                    }
                } else if (ret.toString().trim().length() > 0) {
                    map.put(property, ret.toString());
                }
            }
        }
        return map;
    }

    /**
     * Builds the URL parameter for invoking search services via REST
     * 
     * @param example
     * @return
     * @throws UnsupportedEncodingException 
     */
    public static String buildURLParameters(BaseEntity object) {
        Map<String, Object> map = buildMapValues(object);
        StringBuilder parameter = new StringBuilder("");
        int count = 0;
        try {
            for (String key : map.keySet()) {
                if (count > 0)
                    parameter.append("&");
                parameter.append(key).append("=").append(URLEncoder.encode(map.get(key).toString(), URL_ENCODING));
                count++;
            }
        } catch (UnsupportedEncodingException ex) {
            _log.error(URL_ENCODING + " is not supported", ex);
        }
        if (count > 0)
            return parameter.toString();
        else
            return "";
    }

    /**
     * Builds the query string appended to queryByExample
     * @param example
     * @param exactMatch
     * @return
     */
    @SuppressWarnings("rawtypes")
    public static String buildJpaQueryString(BaseEntity example, boolean exactMatch) {
        int count = 0;
        StringBuilder clause = new StringBuilder(" where ");
        List<String> exampleFields = CacheUtil.getSearchableFields(example);
        for (String property : exampleFields) {
            // get the value
            Object ret = retrieveObjectValue(example, property);
            // append the alias
            property = "obj." + property;
            if (ret != null) {
                if (String.class.isAssignableFrom(ret.getClass()) && !exactMatch) {
                    if (ret.toString().trim().length() > 0) {
                        if (count > 0)
                            clause.append(" and ");
                        clause.append(property).append(" like '%")
                                .append(StringUtil.escapeSql(ret.toString(), true)).append("%'")
                                .append(" escape '\\'");
                        count++;
                    }
                } else if (SystemCodes.class.isAssignableFrom(ret.getClass())) {
                    SystemCodes sc = (SystemCodes) ret;
                    if (!StringUtil.isEmpty(sc.getKey())) {
                        if (count > 0)
                            clause.append(" and ");
                        clause.append(property).append(".key").append(" = '").append(sc.getKey() + "'");
                        count++;
                    }
                } else if (BaseEntity.class.isAssignableFrom(ret.getClass())) {
                    BaseEntity be = (BaseEntity) ret;
                    if (be.getId() != null) {
                        if (count > 0)
                            clause.append(" and ");
                        clause.append(property).append(".id").append(" = ").append(be.getId());
                        count++;
                    }
                } else if (Integer.class.isAssignableFrom(ret.getClass())
                        || Float.class.isAssignableFrom(ret.getClass())
                        || Long.class.isAssignableFrom(ret.getClass())
                        || Double.class.isAssignableFrom(ret.getClass())
                        || BigDecimal.class.isAssignableFrom(ret.getClass())
                        || Boolean.class.isAssignableFrom(ret.getClass())) {
                    // numeric types doesn't need to be enclosed in single quotes
                    if (ret.toString().trim().length() > 0) {
                        if (count > 0)
                            clause.append(" and ");
                        clause.append(property).append(" = ").append(ret.toString());
                        count++;
                    }
                } else if (Class.class.isAssignableFrom(ret.getClass())) {
                    if (count > 0)
                        clause.append(" and ");
                    Class clazz = (Class) ret;
                    clause.append(property).append(" = '").append(clazz.getName()).append("'");
                    count++;
                } else if (Collection.class.isAssignableFrom(ret.getClass())) {
                    // not supported yet
                    _log.warn("FindByExample on type Collection is not supported.");
                } else if (ret.toString().trim().length() > 0) {
                    if (count > 0)
                        clause.append(" and ");
                    clause.append(property).append(" = '").append(StringUtil.escapeSql(ret.toString(), false))
                            .append("'");
                    count++;
                }
            }
        }
        if (count > 0)
            return clause.toString();
        else
            return "";
    }

    /**
     * This method retrieves the object value that corresponds to the property specified.
     * This method can recurse inner classes until specified property is reached.
     * 
     * For example:
     * obj.firstName
     * obj.address.Zipcode
     * 
     * @param obj
     * @param property
     * @return
     */
    @SuppressWarnings("rawtypes")
    public static Object retrieveObjectValue(Object obj, String property) {
        if (property.contains(".")) {
            // we need to recurse down to final object
            String props[] = property.split("\\.");
            try {
                Object ivalue = null;
                if (Map.class.isAssignableFrom(obj.getClass())) {
                    Map map = (Map) obj;
                    ivalue = map.get(props[0]);
                } else {
                    Method method;
                    if (props[0].startsWith("get"))
                        method = obj.getClass().getMethod(props[0]);
                    else
                        method = obj.getClass().getMethod(NamingUtil.toGetterName(props[0]));
                    ivalue = method.invoke(obj);
                }
                if (ivalue == null)
                    return null;
                // traverse collection objects
                if (Collection.class.isAssignableFrom(ivalue.getClass())) {
                    Iterator iter = ((Collection) ivalue).iterator();
                    List<Object> ret = new ArrayList<Object>();
                    String prop = property.substring(props[0].length() + 1);
                    while (iter.hasNext()) {
                        Object lvalue = iter.next();
                        if (lvalue != null) {
                            ret.add(retrieveObjectValue(lvalue, prop));
                        }
                    }
                    return ret;
                }
                return retrieveObjectValue(ivalue, property.substring(props[0].length() + 1));
            } catch (Exception e) {
                throw new InvalidImplementationException("Failed to retrieve value for " + property, e);
            }
        } else {
            // let's get the object value directly
            try {
                if (Map.class.isAssignableFrom(obj.getClass())) {
                    Map map = (Map) obj;
                    return map.get(property);
                } else {
                    Method method;
                    if (property.startsWith("get"))
                        method = obj.getClass().getMethod(property);
                    else
                        method = obj.getClass().getMethod(NamingUtil.toGetterName(property));
                    return method.invoke(obj);
                }
            } catch (Exception e) {
                throw new InvalidImplementationException("Failed to retrieve value for " + property, e);
            }
        }
    }

    /**
     * 
     * This method retrieves the object value that corresponds to the property specified.
     * This method supports nullable fields.
     * 
     * @see retrieveObjectValue
     * @param obj
     * @param property
     * @return
     */
    public static Object retrieveNullableObjectValue(Object obj, String property) {
        try {
            return retrieveObjectValue(obj, property);
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * This method retrieves the object type that corresponds to the property specified.
     * This method can recurse inner classes until specified property is reached.
     * 
     * For example:
     * obj.firstName
     * obj.address.Zipcode
     * 
     * @param obj
     * @param property
     * @return
     */
    public static Class<?> retrieveObjectType(Object obj, String property) {
        if (property.contains(".")) {
            // we need to recurse down to final object
            String props[] = property.split("\\.");
            try {
                Method method = obj.getClass().getMethod(NamingUtil.toGetterName(props[0]));
                Object ivalue = method.invoke(obj);
                return retrieveObjectType(ivalue, property.substring(props[0].length() + 1));
            } catch (Exception e) {
                throw new InvalidImplementationException("Failed to retrieve value for " + property, e);
            }
        } else {
            // let's get the object value directly
            try {
                Method method = obj.getClass().getMethod(NamingUtil.toGetterName(property));
                return method.getReturnType();
            } catch (Exception e) {
                throw new InvalidImplementationException("Failed to retrieve value for " + property, e);
            }
        }
    }

    /**
     * This method evaluates the given expression from the object.
     * This method now uses Spring Expression Language (SpEL).
     * 
     * @param obj
     * @param expression
     * @return
     */
    public static Boolean evaluateExpression(Object obj, String expression) {
        if (StringUtil.isEmpty(expression))
            return false;
        try {
            ExpressionParser parser = new SpelExpressionParser();
            Expression exp = parser.parseExpression(expression);
            return exp.getValue(obj, Boolean.class);
        } catch (Exception e) {
            _log.debug("Failed to evaluate expression [" + expression + "] for object [" + obj.getClass() + "].");
            _log.debug(e.getMessage());
            return false;
        }
    }

    /**
     * This method will replace SQL parameters with
     * respective values from the object.
     * @param sql
     * @param obj
     * @return
     */
    @SuppressWarnings("unchecked")
    public static String replaceSQLParameters(String sql, Object obj) {
        // let's get all sql parameter by expression
        Matcher sqlMatcher = CrudUtil.SQL_PARAM_PATTERN.matcher(sql);
        while (sqlMatcher.find()) {
            String param = sqlMatcher.group(1);
            Object valueObject = CrudUtil.retrieveNullableObjectValue(obj, param);
            if (valueObject == null) {
                sql = sql.replace(sqlMatcher.group(), "null");
            } else if (String.class.isAssignableFrom(valueObject.getClass())) {
                sql = sql.replace(sqlMatcher.group(), "'" + valueObject.toString() + "'");
            } else if (Collection.class.isAssignableFrom(valueObject.getClass())) {
                Collection<Object> list = (Collection<Object>) valueObject;
                int ctr = 0;
                StringBuilder buff = new StringBuilder();
                for (Object item : list) {
                    if (ctr++ > 0)
                        buff.append(", ");
                    if (SystemCodes.class.isAssignableFrom(item.getClass())) {
                        SystemCodes entity = (SystemCodes) item;
                        // use id 
                        buff.append("'").append(entity.getKey()).append("'");
                    } else if (BaseEntity.class.isAssignableFrom(item.getClass())) {
                        BaseEntity entity = (BaseEntity) item;
                        // use id 
                        buff.append(entity.getId());
                    } else
                        buff.append("'").append(item.toString()).append("'");
                }
                sql = sql.replace(sqlMatcher.group(), buff.toString());
            } else
                sql = sql.replace(sqlMatcher.group(), valueObject.toString());
        }
        return sql;
    }

    /**
     * Overloaded method to retrieve all fields, including fields from parent class.
     * @param clazz
     * @return
     */
    @SuppressWarnings({ "rawtypes" })
    public static List<Field> getAllFields(Class clazz) {
        return CrudUtil.getAllFields(clazz, true);
    }

    /**
     * Helper method to retrieve all fields of a class including
     * fields declared in its superclass.
     * @param clazz
     * @param includeParent
     * @return
     */
    @SuppressWarnings({ "rawtypes" })
    public static List<Field> getAllFields(Class clazz, boolean includeParent) {
        List<Field> fields = new ArrayList<Field>();
        if (BaseEntity.class.isAssignableFrom(clazz) && includeParent)
            fields.addAll(getAllFields(clazz.getSuperclass(), includeParent));
        for (Field field : clazz.getDeclaredFields())
            fields.add(field);
        return fields;
    }

    /**
     * Overloaded method to retrieve all methods, including methods from parent class.
     * @param clazz
     * @return
     */
    @SuppressWarnings({ "rawtypes" })
    public static List<Method> getAllMethods(Class clazz) {
        return CrudUtil.getAllMethods(clazz, true);
    }

    /**
     * Helper method to retrieve all methods of a class including
     * methods declared in its superclass.
     * @param clazz
     * @param includeParent
     * @return
     */
    @SuppressWarnings({ "rawtypes" })
    public static List<Method> getAllMethods(Class clazz, boolean includeParent) {
        List<Method> methods = new ArrayList<Method>();
        if (BaseEntity.class.isAssignableFrom(clazz) && includeParent)
            methods.addAll(getAllMethods(clazz.getSuperclass(), includeParent));
        for (Method method : clazz.getDeclaredMethods())
            methods.add(method);
        return methods;
    }

    /**
     * Converts the binding error messages to list of MessageResponse
     * 
     * @param bindingResult
     */
    public static List<MessageResponse> convertErrorMessage(BindingResult bindingResult, Locale locale,
            MessageSource messageSource) {
        List<MessageResponse> errorMessages = new ArrayList<MessageResponse>();
        if (bindingResult.hasErrors()) {
            for (ObjectError error : bindingResult.getAllErrors()) {
                MessageResponse message = null;
                if (error instanceof FieldError) {
                    FieldError ferror = (FieldError) error;
                    message = new MessageResponse(MessageResponse.Type.error, error.getObjectName(),
                            ferror.getField(), error.getCodes(), error.getArguments());
                } else
                    message = new MessageResponse(MessageResponse.Type.error, error.getObjectName(), null,
                            error.getCodes(), error.getArguments());
                message.setMessage(messageSource.getMessage(message, locale));
                errorMessages.add(message);
            }
        }
        return errorMessages;
    }

    /**
     * Builds success message by convention. Success messages are displayed as
     * notifications only.
     * 
     * Standard convention in order of resolving message is: (1)
     * message.<className>.
     * <code>-success (e.g. message.system-codes.add-success)
     *     (2) message.add-success (generic message)
     * 
     * @param elementClass
     * @param object
     * @param code
     * @param locale
     * @return
     */
    public static List<MessageResponse> buildSuccessMessage(BaseEntity object, String code, Locale locale,
            MessageSource messageSource) {
        List<MessageResponse> messages = new ArrayList<MessageResponse>();
        Assert.notNull(object);
        String prefix = "message." + NamingUtil.toElementName(object.getClass().getSimpleName());
        String codes = prefix + "." + code + "-success,message." + code + "-success";
        MessageResponse message = new MessageResponse(MessageResponse.Type.notification, codes.split("\\,"), null);
        message.setMessage(messageSource.getMessage(message, locale));
        messages.add(message);
        return messages;
    }

    /**
     * Helper method to check if the object is a type of 
     * collection or map.
     * 
     * @param ob
     * @return
     */
    public static boolean isCollection(Object ob) {
        return ob instanceof Collection || ob instanceof Map;
    }

}