/*
 * Copyright 2002-2008 the original author or authors.
 *
 * 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.springframework.config.java.internal.factory;

import static java.lang.String.format;
import static org.springframework.config.java.internal.factory.BeanVisibility.PUBLIC;
import static org.springframework.util.StringUtils.hasText;

import java.lang.reflect.Field;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.FatalBeanException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.config.java.annotation.ExternalValue;
import org.springframework.config.java.internal.model.ConfigurationClass;
import org.springframework.config.java.valuesource.CompositeValueResolver;
import org.springframework.config.java.valuesource.ValueResolutionException;
import org.springframework.config.java.valuesource.ValueResolver;


/**
 * BPP automatically registered with {@link JavaConfigBeanFactory} that ensures fields annotated
 * with {@link ExternalValue} get set properly. This functionality complements the support for
 * {@link ExternalValue}-annotated methods, although the implementation approach is significantly
 * different. Methods support is handled through CGLIB subclassing; field support is handled by
 * directly assigning the external value in question to the field during initialization of the
 * declaring Configuration class.
 *
 * <p/><strong>Advantages:</strong>ExternalValue fields are more concise and intuitive than their
 * abstract method counterparts.</p>
 * <strong>Limitations:</strong> ExternalValue-annotated fields will be set once and only once
 * during the bean post-processing phase. This means that in the (theoretical) case of a ValueSource
 * implementation that gets updated dynamically during runtime (e.g.: a database), the ExternalValue
 * field could become stale. In such a case, it is recommended that the user choose ExternalValue
 * methods rather than fields.
 *
 * @author  Chris Beams
 * @see     org.springframework.config.java.internal.enhancement.CglibConfigurationEnhancer;
 */
class ExternalValueInjectingBeanPostProcessor implements BeanPostProcessor {
    private static final Log log = LogFactory.getLog(ExternalValueInjectingBeanPostProcessor.class);
    private final JavaConfigBeanFactory beanFactory;
    private ValueResolver valueResolver;

    /**
     * Creates a new ExternalValueInjectingBeanPostProcessor object.
     *
     * @param  beanFactory  internal bean factory containing {@link ValueResolver} to be used for
     *                      resolving external values.
     */
    public ExternalValueInjectingBeanPostProcessor(JavaConfigBeanFactory beanFactory) {
        this.beanFactory = beanFactory;
    }

    /**
     * no-op.
     */
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; }

    /**
     * Find and process {@link ExternalValue}-annotated fields. Such fields may be of any visibility
     * and declared anywhere throughout this Configuration bean's type hierarchy.
     */
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        // If beanName is null, bypass entirely (this happens in certain cases when using
        // ConfigurationSupport)
        if (beanName == null)
            return bean;

        // Ensure that this bean has a corresponding BeanDefinition
        // (i.e.: it may have been registered directly as a singleton)
        if (!beanFactory.containsBeanDefinition(beanName, PUBLIC))
            return bean;

        // Proceed only for @Configuration bean definitions
        BeanDefinition beanDef = beanFactory.getBeanDefinition(beanName, PUBLIC);
        if (!beanDef.hasAttribute(ConfigurationClass.IS_CONFIGURATION_CLASS))
            return bean;

        // Cascade up the type hierarchy and introspect for @ExternalValue fields
        Class<?> currentClass = bean.getClass();
        try {
            do {
                Field[] fields = currentClass.getDeclaredFields();
                for (Field field : fields) {
                    ExternalValue evAnno = field.getAnnotation(ExternalValue.class);
                    if (evAnno == null)
                        continue;

                    if (valueResolver == null)
                        valueResolver = CompositeValueResolver.forMember(beanFactory.getParentBeanFactory(), field);

                    field.setAccessible(true);
                    String valueName = hasText(evAnno.value()) ? evAnno.value() : field.getName();
                    try {
                        Object value = valueResolver.resolve(valueName, field.getType());
                        field.set(bean, value);
                    } catch (ValueResolutionException ex) {
                        // does this field have a default value?
                        // if not, propagate the value resolution exception
                        Object defaultValue = field.get(bean);
                        if (defaultValue == null)
                            throw ex;

                        log.info(format("No external value could be found for %s.%s field, "
                                        + "falling back to its default value '%s'",
                                        currentClass.getSimpleName(), field.getName(), defaultValue));
                    }
                }

                currentClass = currentClass.getSuperclass();
            } while (currentClass != null);
        } catch (NoSuchMethodError ex) {
            throw new FatalBeanException("Programmer error: could not find method via reflection", ex);
        } catch (IllegalAccessException ex) {
            throw new FatalBeanException("Programmer error: illegal access, forgot to use setAccessible(true)?", ex);
        }

        return bean;
    }

}
