/*
 * Copyright (C) 2004-2008 Jive Software. All rights reserved.
 *
 * Licensed 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.jivesoftware.util;

import java.awt.Color;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A utility class that provides methods that are useful for dealing with
 * Java Beans.
 */
public class BeanUtils {

    private static final Logger Log = LoggerFactory.getLogger(BeanUtils.class);

    /**
     * The date format recognized for parsing/formattig dates.
     */
    public static final String DATE_FORMAT = "MM/dd/yyyy";

    private static DateFormat dateFormatter = new SimpleDateFormat(DATE_FORMAT);

    /**
     * Sets the properties of a Java Bean based on the String name/value pairs in
     * the specifieed Map. Because this method has to know how to convert a
     * String value into the correct type for the bean, only a few bean property
     * types are supported. They are: String, boolean, int, long, float, double,
     * Color, and Class.<p>
     *
     * If key/value pairs exist in the Map that don't correspond to properties
     * of the bean, they will be ignored.
     *
     * @param bean the JavaBean to set properties on.
     * @param properties String name/value pairs of the properties to set.
     */
    public static void setProperties(Object bean, Map<String, String> properties) {
        try {
            // Loop through all the property names in the Map
            for (String propName : properties.keySet()) {
                try {
                    // Create a property descriptor for the named property. If
                    // the bean doesn't have the named property, an
                    // Introspection will be thrown.
                    PropertyDescriptor descriptor = new PropertyDescriptor(
                            propName, bean.getClass());
                    // Load the class type of the property.
                    Class propertyType = descriptor.getPropertyType();
                    // Get the value of the property by converting it from a
                    // String to the correct object type.
                    Object value = decode(propertyType, properties.get(propName));
                    // Set the value of the bean.
                    descriptor.getWriteMethod().invoke(bean, value);
                }
                catch (IntrospectionException ie) {
                    // Ignore. This exception means that the key in the map
                    // does not correspond to a property of the bean.
                }
                catch (InvocationTargetException ite) {
                    // Ignore. This exception most often occurs when a
                    // value in the map is null and the target method doesn't
                    // support null properties.
                }
            }
        }
        catch (Exception e) {
            Log.error(e.getMessage(), e);
        }
    }

    /**
     * Sets the properties of a Java Bean based on the request's properties. Because
     * this method has to know how to convert a String value into the correct type
     * for the bean, only a few bean property types are supported. They are: String,
     * boolean, int, long, float, double, Color, and Class.<p>
     *
     * If key/value pairs exist in the Map that don't correspond to properties
     * of the bean, they will be ignored.
     *
     * @param bean the JavaBean to set properties on.
     * @param request the HTTP request.
     */
    public static void setProperties(Object bean, HttpServletRequest request) {
        for (Enumeration propNames = request.getParameterNames(); propNames.hasMoreElements();) {
            String propName = (String) propNames.nextElement();
            try {
                // Create a property descriptor for the named property. If
                // the bean doesn't have the named property, an
                // Introspection will be thrown.
                PropertyDescriptor descriptor = new PropertyDescriptor(
                        propName, bean.getClass());
                // Load the class type of the property.
                Class propertyType = descriptor.getPropertyType();
                // Get the value of the property by converting it from a
                // String to the correct object type.
                Object value = decode(propertyType, request.getParameter(propName));
                // Set the value of the bean.
                descriptor.getWriteMethod().invoke(bean, value);
            }
            catch (IntrospectionException ie) {
                // Ignore. This exception means that the key in the map
                // does not correspond to a property of the bean.
            }
            catch (InvocationTargetException ite) {
                // Ignore. This exception most often occurs when a
                // value in the map is null and the target method doesn't
                // support null properties.
            }
            catch (IllegalAccessException e) {
                Log.error(e.getMessage(), e);
            }
            catch (Exception e) {
                Log.error(e.getMessage(), e);
            }
        }
    }

    /**
     * Gets the properties from a Java Bean and returns them in a Map of String
     * name/value pairs. Because this method has to know how to convert a
     * bean property into a String value, only a few bean property
     * types are supported. They are: String, boolean, int, long, float, double,
     * Color, and Class.
     *
     * @param bean a Java Bean to get properties from.
     * @return a Map of all properties as String name/value pairs.
     */
    public static Map<String, String> getProperties(Object bean) {
        Map<String, String> properties = new HashMap<>();
        try {
            BeanInfo beanInfo = Introspector.getBeanInfo(bean.getClass());
            // Loop through all properties of the bean.
            PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors();
            String [] names = new String[descriptors.length];
            for (int i=0; i<names.length; i++) {
                // Determine the property name.
                String name = descriptors[i].getName();
                //Class type = descriptors[i].getPropertyType();
                // Decode the property value using the property type and
                // encoded String value.
                Object value = descriptors[i].getReadMethod().invoke(bean,(java.lang.Object[]) null);
                // Add to Map, encoding the value as a String.
                properties.put(name, encode(value));
            }
        }
        catch (Exception e) {
            Log.error(e.getMessage(), e);
        }
        return properties;
    }

    /**
     * Returns the PropertyDescriptor array for the specified Java Bean Class.
     * The method also does a special check to see of the bean has a BeanInfo
     * class that extends the JiveBeanInfo class. If yes, we load the
     * PropertyDescriptor array directly from that BeanInfo class rather than
     * through the Introspector in order to preserve the desired ordering of
     * properties.
     *
     * @param beanClass the Class of the JavaBean.
     * @return the PropertyDescriptor array for the specified Java Bean Class.
     * @throws java.beans.IntrospectionException
     */
    public static PropertyDescriptor[] getPropertyDescriptors(Class beanClass)
            throws IntrospectionException
    {
        // See if the Java Bean has a BeanInfo class that implements
        // JiveBeanInfo. If so, return the PropertyDescriptor from that
        // class. This will bypass properties of parent classes, but this is
        // the normal behavior of classes that implement JiveBeanInfo.
        try {
            JiveBeanInfo beanInfo = (JiveBeanInfo)ClassUtils.forName(
                    beanClass.getName() + "BeanInfo").newInstance();
            return beanInfo.getPropertyDescriptors();
        }
        catch (Exception e) {
            // Ignore.
        }
        // Otherwise, return the PropertyDescriptors from the Introspector.
        return Introspector.getBeanInfo(beanClass).getPropertyDescriptors();
    }

    /**
     * Encodes a bean property value as a String. If the object type is not
     * supported, null will be returned.
     *
     * @param value an Object to encode in a String representation.
     * @return the encoded bean.
     */
    private static String encode(Object value) {
        if (value instanceof String) {
            return (String)value;
        }
        if (value instanceof Boolean ||
            value instanceof Integer ||
            value instanceof Long ||
            value instanceof Float ||
            value instanceof Double)
        {
            return value.toString();
        }
        if (value instanceof Date) {
            try {
                return dateFormatter.format((Date)value);
            }
            catch (Exception ignored) {
                // Ignore.
            }
        }
        if (value instanceof Color) {
            Color color = (Color)value;
            return color.getRed() +","+ color.getGreen() +","+ color.getBlue();
        }
        if (value instanceof Class) {
            return ((Class)value).getName();
        }
        return null;
    }

    /**
     * Decodes a String into an object of the specified type. If the object
     * type is not supported, null will be returned.
     *
     * @param type the type of the property.
     * @param value the encode String value to decode.
     * @return the String value decoded into the specified type.
     * @throws Exception
     */
    private static Object decode(Class type, String value) throws Exception {
        if (type.getName().equals("java.lang.String")) {
            return value;
        }
        if (type.getName().equals("boolean")) {
            return Boolean.valueOf(value);
        }
        if (type.getName().equals("int")) {
            return Integer.valueOf(value);
        }
        if (type.getName().equals("long")) {
            return Long.valueOf(value);
        }
        if (type.getName().equals("float")) {
            return Float.valueOf(value);
        }
        if (type.getName().equals("double")) {
            return Double.valueOf(value);
        }
        if (type.getName().equals("java.util.Date")) {
            try {
                return dateFormatter.parse(value);
            }
            catch (Exception ignored) {
                // Ignore.
            }
        }
        if (type.getName().equals("java.awt.Color")) {
            StringTokenizer tokens = new StringTokenizer(value, ",");
            int red = Integer.parseInt(tokens.nextToken());
            int green = Integer.parseInt(tokens.nextToken());
            int blue = Integer.parseInt(tokens.nextToken());
            return new Color(red, green, blue);
        }
        if (type.getName().equals("java.lang.Class")) {
            return ClassUtils.forName(value);
        }
        return null;
    }

    // This class is not instantiable.
    private BeanUtils() {
        // do nothing.
    }
}
