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

import static java.lang.String.format;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.config.java.internal.enhancement.CglibConfigurationEnhancer;
import org.springframework.config.java.internal.enhancement.ConfigurationEnhancer;
import org.springframework.config.java.internal.factory.BeanFactoryProvider;
import org.springframework.config.java.internal.factory.DefaultJavaConfigBeanFactory;
import org.springframework.config.java.internal.factory.JavaConfigBeanFactory;
import org.springframework.config.java.internal.factory.support.AsmJavaConfigBeanDefinitionReader;
import org.springframework.config.java.internal.factory.support.JavaConfigBeanDefinitionReader;
import org.springframework.config.java.internal.model.ConfigurationClass;
import org.springframework.config.java.naming.BeanNamingStrategy;
import org.springframework.config.java.util.BeanMetadata;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.ClassUtils;


/** TODO: JAVADOC */
public class InternalConfigurationPostProcessor implements BeanFactoryPostProcessor {

    private static final Log logger = LogFactory.getLog(InternalConfigurationPostProcessor.class);

    private final AbstractApplicationContext externalContext;
    private final BeanNamingStrategy beanNamingStrategy;
    private final BeanFactoryProvider beanFactoryProvider;

    private final ArrayList<String> ignoredBeanPostProcessors = new ArrayList<String>();


    public InternalConfigurationPostProcessor(AbstractApplicationContext enclosingContext,
                                              BeanNamingStrategy beanNamingStrategy,
                                              BeanFactoryProvider beanFactoryProvider) {
        this.externalContext = enclosingContext;
        this.beanNamingStrategy = beanNamingStrategy;
        this.beanFactoryProvider = beanFactoryProvider;
    }

    public void postProcessBeanFactory(ConfigurableListableBeanFactory externalBeanFactory) throws BeansException {
        JavaConfigBeanFactory internalBeanFactory = createInternalBeanFactory(externalBeanFactory);

        parseAnyConfigurationClasses(externalBeanFactory, internalBeanFactory);

        enhanceAnyConfigurationClasses(externalBeanFactory, internalBeanFactory);
    }

    public void addIgnoredBeanPostProcessor(String bppClassName) {
        ignoredBeanPostProcessors.add(bppClassName);
    }

    private JavaConfigBeanFactory createInternalBeanFactory(ConfigurableListableBeanFactory externalBeanFactory) {
        DefaultJavaConfigBeanFactory internalBeanFactory =
            new DefaultJavaConfigBeanFactory(externalBeanFactory, beanFactoryProvider);
        if (beanNamingStrategy != null)
            internalBeanFactory.setBeanNamingStrategy(beanNamingStrategy);

        wrapInternalBeanFactoryInApplicationContext(internalBeanFactory);

        return internalBeanFactory;
    }

    private void wrapInternalBeanFactoryInApplicationContext(DefaultJavaConfigBeanFactory internalBeanFactory) {
        final AbstractApplicationContext internalContext = new GenericApplicationContext(internalBeanFactory);
        internalContext.setDisplayName("JavaConfig internal application context");

        // add a listener that triggers this child context to refresh when the parent refreshes.
        // this will cause any BeanPostProcessors / BeanFactoryPostProcessors to be beansInvokedFor
        // on the beans in the child context.
        externalContext.addApplicationListener(new ChildContextRefreshingListener(externalContext, internalContext,
                                                                                  ignoredBeanPostProcessors));
    }

    private void parseAnyConfigurationClasses(ConfigurableListableBeanFactory externalBeanFactory,
                                              JavaConfigBeanFactory internalBeanFactory) {
        
        // linked map is important for maintaining predictable ordering of configuration classes.
        // this is important in bean / value override situations.
        LinkedHashMap<String, ClassPathResource> configClassResources = new LinkedHashMap<String, ClassPathResource>();

        for (String beanName : externalBeanFactory.getBeanDefinitionNames()) {
            BeanDefinition beanDef = externalBeanFactory.getBeanDefinition(beanName);
            if (beanDef.isAbstract())
                continue;

            if (beanDef.hasAttribute(ConfigurationClass.IS_CONFIGURATION_CLASS)) {
                String path = ClassUtils.convertClassNameToResourcePath(beanDef.getBeanClassName());
                configClassResources.put(beanName, new ClassPathResource(path));
            }
        }

        JavaConfigBeanDefinitionReader reader;
                reader = new AsmJavaConfigBeanDefinitionReader(internalBeanFactory);

        reader.loadBeanDefinitions(configClassResources);
    }

    /**
     * Post-processes a BeanFactory in search of Configuration class BeanDefinitions; any candidates
     * are then enhanced by a {@link ConfigurationEnhancer}. Candidate status is determined by
     * BeanDefinition attribute metadata.
     *
     * @author  Chris Beams
     * @see     ConfigurationClass#IS_CONFIGURATION_CLASS
     * @see     ConfigurationEnhancer
     * @see     BeanFactoryPostProcessor
     */
    private void enhanceAnyConfigurationClasses(ConfigurableListableBeanFactory externalBeanFactory,
                                                JavaConfigBeanFactory internalBeanFactory) {
        ConfigurationEnhancer enhancer = new CglibConfigurationEnhancer(internalBeanFactory);

        int configClassesEnhanced = 0;

        for (String beanName : externalBeanFactory.getBeanDefinitionNames()) {
            BeanDefinition beanDef = externalBeanFactory.getBeanDefinition(beanName);

            // is the beanDef marked as representing a configuration class?
            if (!beanDef.hasAttribute(ConfigurationClass.IS_CONFIGURATION_CLASS))
                continue;

            String configClassName = beanDef.getBeanClassName();

            String enhancedClassName = enhancer.enhance(configClassName);

            if (logger.isDebugEnabled())
                logger.debug(String.format("Replacing bean definition '%s' existing class name '%s' with enhanced class name '%s'",
                                           beanName, configClassName, enhancedClassName));

            beanDef.setBeanClassName(enhancedClassName);

            configClassesEnhanced++;
        }

        if (configClassesEnhanced == 0)
            logger.warn("Found no @Configuration class BeanDefinitions within " + internalBeanFactory);
    }

}

/**
 * Listens for {@link ContextRefreshedEvent ContextRefreshedEvents} sourced from a parent
 * application context and propagates that event to the child. Just prior to refreshing the child,
 * searches for and adds any {@link BeanFactoryPostProcessor} and {@link BeanPostProcessor} objects
 * found in the parent context's {@link BeanFactory}
 *
 * @author  Chris Beams
 */
class ChildContextRefreshingListener implements ApplicationListener {

    private static final Log logger = LogFactory.getLog(ChildContextRefreshingListener.class);
    private final AbstractApplicationContext parent;
    private final AbstractApplicationContext child;
    private final List<String> ignoredBeanPostProcessors;

    public ChildContextRefreshingListener(AbstractApplicationContext parent, AbstractApplicationContext child,
                                          List<String> ignoredBeanPostProcessors) {
        this.parent = parent;
        this.child = child;
        this.ignoredBeanPostProcessors = ignoredBeanPostProcessors;
    }

    public void onApplicationEvent(ApplicationEvent event) {
        // only respond to ContextRefreshedEvents that are sourced from the parent context
        if (event.getSource() != parent)
            return;

        if (event instanceof ContextRefreshedEvent) {
            if (logger.isDebugEnabled())
                logger.debug(format("Caught ContextRefreshedEvent from parent application context [%s], now refreshing [%s]",
                                    parent.getDisplayName(), child.getDisplayName()));

            copyBeanFactoryPostProcessors();

            child.setParent(parent);
            child.refresh();
            return;
        }

        if (event instanceof ContextClosedEvent) {
            if (logger.isDebugEnabled())
                logger.debug(format("Caught ContextClosedEvent from parent application context [%s], now closing [%s]",
                                    parent.getDisplayName(), child.getDisplayName()));

            child.close();
            return;
        }
    }

    /**
     * Copy {@link BeanFactoryPostProcessor} instances from parent context to child. Note that
     * copying {@link BeanPostProcessor} instances is not necessary (see link below)
     *
     * @see  DefaultJavaConfigBeanFactory#getBeanPostProcessors()
     */
    private void copyBeanFactoryPostProcessors() {
        // Both of the following loops are necessary because BFPPs may have been added
        // programmatically, e.g.: via addBeanFactoryPostProcessor(), or declaratively via bean
        // definitions.  We must iterate over both possibilities in order to find all potential post
        // processors.


        // First, copy all programmatically registered BFPPs.  Note that this means
        // programmatically registered BFPPs are *always* propagated to the internal
        // bean factory, with no concern given to COPY_HIDDEN metadata as below.
        for (Object bfpp : parent.getBeanFactoryPostProcessors())
            doCopyBeanFactoryPostProcessor((BeanFactoryPostProcessor) bfpp);


        // Second, copy all BFPPs registered via bean definition that have the
        // COPY_HIDDEN metadata attribute
        ConfigurableListableBeanFactory parentBF = parent.getBeanFactory();

        @SuppressWarnings("unchecked")
        Map<String, BeanFactoryPostProcessor> bfpps = parentBF.getBeansOfType(BeanFactoryPostProcessor.class);

        for (String bfppName : bfpps.keySet()) {

            // It's possible a BFPP instance may have been registered programmatically
            // with registerSingleton(). If so, skip it because it will have no bean
            // definition and thus no COPY_HIDDEN metadata to interrogate
            if (!parentBF.containsBeanDefinition(bfppName))
                continue;

            // BFPPs must be explicitly attributed with COPY_HIDDEN metadata in order
            // to be propagated to the internal bean factory.  This is by design because
            // indiscriminate propagation may violate assumptions a BFPP has about the
            // BeanFactories it processes.  See SJC-113 for an example.
            if (parentBF.getBeanDefinition(bfppName).getAttribute(BeanMetadata.COPY_HIDDEN) == null)
                continue;

            // it is important that the actual BFPP *instance* is copied rather than
            // just the bean definition, because some BFPPs be stateful.
            doCopyBeanFactoryPostProcessor(bfpps.get(bfppName));
        }

    }

    private void doCopyBeanFactoryPostProcessor(BeanFactoryPostProcessor postProcessor) {
        if (ignoredBeanPostProcessors.contains(postProcessor.getClass().getName()))
            return;

        logger.debug(String.format("copying BeanFactoryPostProcessor %s to child context %s", postProcessor, child));
        child.addBeanFactoryPostProcessor(postProcessor);
    }

}
