/*
 * 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>
 * 
 * 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();
	}
    }
}
