/*
 * 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.
 */

// Contributors:  Georg Lundesgaard

package org.apache.log4j.config;

import org.apache.log4j.Appender;
import org.apache.log4j.Level;
import org.apache.log4j.Priority;
import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.helpers.OptionConverter;
import org.apache.log4j.spi.OptionHandler;
import org.apache.log4j.spi.ErrorHandler;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.InterruptedIOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Enumeration;
import java.util.Properties;

/**
 * General purpose Object property setter. Clients repeatedly invokes {@link #setProperty setProperty(name,value)} in
 * order to invoke setters on the Object specified in the constructor. This class relies on the JavaBeans
 * {@link Introspector} to analyze the given Object Class using reflection.
 *
 * <p>
 * Usage:
 *
 * <pre>
 * PropertySetter ps = new PropertySetter(anObject);
 * ps.set("name", "Joe");
 * ps.set("age", "32");
 * ps.set("isMale", "true");
 * </pre>
 * <p>
 * will cause the invocations anObject.setName("Joe"), anObject.setAge(32), and setMale(true) if such methods exist with
 * those signatures. Otherwise an {@link IntrospectionException} are thrown.
 *
 * @author Anders Kristensen
 * @since 1.1
 */
public class PropertySetter {
    protected Object obj;
    protected PropertyDescriptor[] props;

    /**
     * Create a new PropertySetter for the specified Object. This is done in prepartion for invoking
     * {@link #setProperty} one or more times.
     *
     * @param obj the object for which to set properties
     */
    public PropertySetter(Object obj) {
        this.obj = obj;
    }

    /**
     * Uses JavaBeans {@link Introspector} to computer setters of object to be configured.
     */
    protected void introspect() {
        try {
            BeanInfo bi = Introspector.getBeanInfo(obj.getClass());
            props = bi.getPropertyDescriptors();
        } catch (IntrospectionException ex) {
            LogLog.error("Failed to introspect " + obj + ": " + ex.getMessage());
            props = new PropertyDescriptor[0];
        }
    }

    /**
     * Set the properties of an object passed as a parameter in one go. The
     * <code>properties</code> are parsed relative to a <code>prefix</code>.
     *
     * @param obj        The object to configure.
     * @param properties A java.util.Properties containing keys and values.
     * @param prefix     Only keys having the specified prefix will be set.
     */
    public static void setProperties(Object obj, Properties properties, String prefix) {
        new PropertySetter(obj).setProperties(properties, prefix);
    }

    /**
     * Set the properites for the object that match the <code>prefix</code> passed as parameter.
     */
    public void setProperties(Properties properties, String prefix) {
        int len = prefix.length();

        for (Enumeration e = properties.propertyNames(); e.hasMoreElements(); ) {
            String key = (String) e.nextElement();

            // handle only properties that start with the desired frefix.
            if (key.startsWith(prefix)) {

                // ignore key if it contains dots after the prefix
                if (key.indexOf('.', len + 1) > 0) {
                    // System.err.println("----------Ignoring---["+key
                    // +"], prefix=["+prefix+"].");
                    continue;
                }

                String value = OptionConverter.findAndSubst(key, properties);
                key = key.substring(len);
                if (("layout".equals(key) || "errorhandler".equals(key)) && obj instanceof Appender) {
                    continue;
                }
                //
                // if the property type is an OptionHandler
                // (for example, triggeringPolicy of
                // org.apache.log4j.rolling.RollingFileAppender)
                PropertyDescriptor prop = getPropertyDescriptor(Introspector.decapitalize(key));
                if (prop != null && OptionHandler.class.isAssignableFrom(prop.getPropertyType())
                        && prop.getWriteMethod() != null) {
                    OptionHandler opt = (OptionHandler) OptionConverter.instantiateByKey(properties, prefix + key,
                            prop.getPropertyType(), null);
                    PropertySetter setter = new PropertySetter(opt);
                    setter.setProperties(properties, prefix + key + ".");
                    try {
                        prop.getWriteMethod().invoke(this.obj, new Object[] { opt });
                    } catch (IllegalAccessException ex) {
                        LogLog.warn("Failed to set property [" + key + "] to value \"" + value + "\". ", ex);
                    } catch (InvocationTargetException ex) {
                        if (ex.getTargetException() instanceof InterruptedException
                                || ex.getTargetException() instanceof InterruptedIOException) {
                            Thread.currentThread().interrupt();
                        }
                        LogLog.warn("Failed to set property [" + key + "] to value \"" + value + "\". ", ex);
                    } catch (RuntimeException ex) {
                        LogLog.warn("Failed to set property [" + key + "] to value \"" + value + "\". ", ex);
                    }
                    continue;
                }

                setProperty(key, value);
            }
        }
        activate();
    }

    /**
     * Set a property on this PropertySetter's Object. If successful, this method will invoke a setter method on the
     * underlying Object. The setter is the one for the specified property name and the value is determined partly from
     * the setter argument type and partly from the value specified in the call to this method.
     *
     * <p>
     * If the setter expects a String no conversion is necessary. If it expects an int, then an attempt is made to
     * convert 'value' to an int using new Integer(value). If the setter expects a boolean, the conversion is by new
     * Boolean(value).
     *
     * @param name  name of the property
     * @param value String value of the property
     */
    public void setProperty(String name, String value) {
        if (value == null)
            return;

        name = Introspector.decapitalize(name);
        PropertyDescriptor prop = getPropertyDescriptor(name);

        // LogLog.debug("---------Key: "+name+", type="+prop.getPropertyType());

        if (prop == null) {
            LogLog.warn("No such property [" + name + "] in " + obj.getClass().getName() + ".");
        } else {
            try {
                setProperty(prop, name, value);
            } catch (PropertySetterException ex) {
                LogLog.warn("Failed to set property [" + name + "] to value \"" + value + "\". ", ex.rootCause);
            }
        }
    }

    /**
     * Set the named property given a {@link PropertyDescriptor}.
     *
     * @param prop  A PropertyDescriptor describing the characteristics of the property to set.
     * @param name  The named of the property to set.
     * @param value The value of the property.
     */
    public void setProperty(PropertyDescriptor prop, String name, String value) throws PropertySetterException {
        Method setter = prop.getWriteMethod();
        if (setter == null) {
            throw new PropertySetterException("No setter for property [" + name + "].");
        }
        Class[] paramTypes = setter.getParameterTypes();
        if (paramTypes.length != 1) {
            throw new PropertySetterException("#params for setter != 1");
        }

        Object arg;
        try {
            arg = convertArg(value, paramTypes[0]);
        } catch (Throwable t) {
            throw new PropertySetterException("Conversion to type [" + paramTypes[0] + "] failed. Reason: " + t);
        }
        if (arg == null) {
            throw new PropertySetterException("Conversion to type [" + paramTypes[0] + "] failed.");
        }
        LogLog.debug("Setting property [" + name + "] to [" + arg + "].");
        try {
            setter.invoke(obj, new Object[] { arg });
        } catch (IllegalAccessException ex) {
            throw new PropertySetterException(ex);
        } catch (InvocationTargetException ex) {
            if (ex.getTargetException() instanceof InterruptedException
                    || ex.getTargetException() instanceof InterruptedIOException) {
                Thread.currentThread().interrupt();
            }
            throw new PropertySetterException(ex);
        } catch (RuntimeException ex) {
            throw new PropertySetterException(ex);
        }
    }

    /**
     * Convert <code>val</code> a String parameter to an object of a given type.
     */
    protected Object convertArg(String val, Class type) {
        if (val == null)
            return null;

        String v = val.trim();
        if (String.class.isAssignableFrom(type)) {
            return val;
        } else if (Integer.TYPE.isAssignableFrom(type)) {
            return new Integer(v);
        } else if (Long.TYPE.isAssignableFrom(type)) {
            return new Long(v);
        } else if (Boolean.TYPE.isAssignableFrom(type)) {
            if ("true".equalsIgnoreCase(v)) {
                return Boolean.TRUE;
            } else if ("false".equalsIgnoreCase(v)) {
                return Boolean.FALSE;
            }
        } else if (Priority.class.isAssignableFrom(type)) {
            return OptionConverter.toLevel(v, (Level) Level.DEBUG);
        } else if (ErrorHandler.class.isAssignableFrom(type)) {
            return OptionConverter.instantiateByClassName(v, ErrorHandler.class, null);
        }
        return null;
    }

    protected PropertyDescriptor getPropertyDescriptor(String name) {
        if (props == null)
            introspect();

        for (int i = 0; i < props.length; i++) {
            if (name.equals(props[i].getName())) {
                return props[i];
            }
        }
        return null;
    }

    public void activate() {
        if (obj instanceof OptionHandler) {
            ((OptionHandler) obj).activateOptions();
        }
    }
}
