/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.repo.management.subsystems;

import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.BeanReference;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.config.TypedStringValue;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;

/**
 * A {@link BeanFactoryPostProcessor} that upgrades old-style Spring overrides that add location paths to the
 * <code>repository-properties</code> or <code>hibernateConfigProperties</code> beans to instead add these paths to the
 * <code>global-properties</code> bean. To avoid the warning messages output by this class, new property overrides
 * should be added to alfresco-global.properties without overriding any bean definitions.
 * 
 * @author dward
 */
public class LegacyConfigPostProcessor implements BeanFactoryPostProcessor, PriorityOrdered
{
    /** The name of the bean that, in new configurations, holds all properties */
    private static final String BEAN_NAME_GLOBAL_PROPERTIES = "global-properties";

    /** The name of the bean that expands repository properties. These should now be defaulted from global-properties. */
    private static final String BEAN_NAME_REPOSITORY_PROPERTIES = "repository-properties";

    /** The name of the bean that holds hibernate properties. These should now be overriden by global-properties. */
    private static final String BEAN_NAME_HIBERNATE_PROPERTIES = "hibernateConfigProperties";

    /** The name of the property on a Spring property loader that holds a list of property file location paths. */
    private static final String PROPERTY_LOCATIONS = "locations";

    /** The name of the property on a Spring property loader that holds a local property map. */
    private static final String PROPERTY_PROPERTIES = "properties";

    /** The logger. */
    private static Log logger = LogFactory.getLog(LegacyConfigPostProcessor.class);

    /*
     * (non-Javadoc)
     * @see
     * org.springframework.beans.factory.config.BeanFactoryPostProcessor#postProcessBeanFactory(org.springframework.
     * beans.factory.config.ConfigurableListableBeanFactory)
     */
    @SuppressWarnings("unchecked")
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException
    {
        try
        {
            // Look up the global-properties bean and its locations list
            MutablePropertyValues globalProperties = beanFactory.getBeanDefinition(
                    LegacyConfigPostProcessor.BEAN_NAME_GLOBAL_PROPERTIES).getPropertyValues();
            PropertyValue pv = globalProperties.getPropertyValue(LegacyConfigPostProcessor.PROPERTY_LOCATIONS);
            Collection<Object> globalPropertyLocations;
            Object value;

            // Use the locations list if there is one, otherwise associate a new empty list
            if (pv != null && (value = pv.getValue()) != null && value instanceof Collection)
            {
                globalPropertyLocations = (Collection<Object>) value;
            }
            else
            {
                globalPropertyLocations = new ManagedList(10);
                globalProperties
                        .addPropertyValue(LegacyConfigPostProcessor.PROPERTY_LOCATIONS, globalPropertyLocations);
            }

            // Move location paths added to repository-properties
            MutablePropertyValues repositoryProperties = processLocations(beanFactory, globalPropertyLocations,
                    LegacyConfigPostProcessor.BEAN_NAME_REPOSITORY_PROPERTIES, new String[]
                    {
                        "classpath:alfresco/version.properties"
                    });
            // Fix up additional properties to enforce correct order of precedence
            repositoryProperties.addPropertyValue("ignoreUnresolvablePlaceholders", Boolean.TRUE);
            repositoryProperties.addPropertyValue("localOverride", Boolean.FALSE);
            repositoryProperties.addPropertyValue("valueSeparator", null);
            repositoryProperties.addPropertyValue("systemPropertiesModeName", "SYSTEM_PROPERTIES_MODE_NEVER");

            // Move location paths added to hibernateConfigProperties
            MutablePropertyValues hibernateProperties = processLocations(beanFactory, globalPropertyLocations,
                    LegacyConfigPostProcessor.BEAN_NAME_HIBERNATE_PROPERTIES, new String[]
                    {
                        "classpath:alfresco/domain/hibernate-cfg.properties",
                        "classpath*:alfresco/enterprise/cache/hibernate-cfg.properties"
                    });
            // Fix up additional properties to enforce correct order of precedence
            hibernateProperties.addPropertyValue("localOverride", Boolean.TRUE);

            // Because Spring gets all post processors in one shot, the bean may already have been created. Let's try to
            // fix it up!
            PropertyPlaceholderConfigurer repositoryConfigurer = (PropertyPlaceholderConfigurer) beanFactory
                    .getSingleton(LegacyConfigPostProcessor.BEAN_NAME_REPOSITORY_PROPERTIES);
            if (repositoryConfigurer != null)
            {
                // Reset locations list
                repositoryConfigurer.setLocations(null);

                // Invalidate cached merged bean definitions
                ((BeanDefinitionRegistry) beanFactory).registerBeanDefinition(
                        LegacyConfigPostProcessor.BEAN_NAME_REPOSITORY_PROPERTIES, beanFactory
                                .getBeanDefinition(LegacyConfigPostProcessor.BEAN_NAME_REPOSITORY_PROPERTIES));

                // Reconfigure the bean according to its new definition
                beanFactory.configureBean(repositoryConfigurer,
                        LegacyConfigPostProcessor.BEAN_NAME_REPOSITORY_PROPERTIES);
            }
        }
        catch (NoSuchBeanDefinitionException e)
        {
            // Ignore and continue
        }
    }

    /**
     * Given a bean name (assumed to implement {@link org.springframework.core.io.support.PropertiesLoaderSupport})
     * checks whether it already references the <code>global-properties</code> bean. If not, 'upgrades' the bean by
     * appending all additional resources it mentions in its <code>locations</code> property to
     * <code>globalPropertyLocations</code>, except for those resources mentioned in <code>newLocations</code>. A
     * reference to <code>global-properties</code> will then be added and the resource list in
     * <code>newLocations<code> will then become the new <code>locations</code> list for the bean.
     * 
     * @param beanFactory
     *            the bean factory
     * @param globalPropertyLocations
     *            the list of global property locations to be appended to
     * @param beanName
     *            the bean name
     * @param newLocations
     *            the new locations to be set on the bean
     * @return the mutable property values
     */
    @SuppressWarnings("unchecked")
    private MutablePropertyValues processLocations(ConfigurableListableBeanFactory beanFactory,
            Collection<Object> globalPropertyLocations, String beanName, String[] newLocations)
    {
        // Get the bean an check its existing properties value
        MutablePropertyValues beanProperties = beanFactory.getBeanDefinition(beanName).getPropertyValues();
        PropertyValue pv = beanProperties.getPropertyValue(LegacyConfigPostProcessor.PROPERTY_PROPERTIES);
        Object value;

        // If the properties value already references the global-properties bean, we have nothing else to do. Otherwise,
        // we have to 'upgrade' the bean definition.
        if (pv == null || (value = pv.getValue()) == null || !(value instanceof BeanReference)
                || ((BeanReference) value).getBeanName().equals(LegacyConfigPostProcessor.BEAN_NAME_GLOBAL_PROPERTIES))
        {
            // Convert the array of new locations to a managed list of type string values, so that it is
            // compatible with a bean definition
            Collection<Object> newLocationList = new ManagedList(newLocations.length);
            if (newLocations != null && newLocations.length > 0)
            {
                for (String preserveLocation : newLocations)
                {
                    newLocationList.add(new TypedStringValue(preserveLocation));
                }
            }

            // If there is currently a locations list, process it
            pv = beanProperties.getPropertyValue(LegacyConfigPostProcessor.PROPERTY_LOCATIONS);
            if (pv != null && (value = pv.getValue()) != null && value instanceof Collection)
            {
                Collection<Object> locations = (Collection<Object>) value;

                // Compute the set of locations that need to be added to globalPropertyLocations (preserving order) and
                // warn about each
                Set<Object> addedLocations = new LinkedHashSet<Object>(locations);
                addedLocations.removeAll(globalPropertyLocations);
                addedLocations.removeAll(newLocationList);

                for (Object location : addedLocations)
                {
                    LegacyConfigPostProcessor.logger.warn("Legacy configuration detected: adding "
                            + (location instanceof TypedStringValue ? ((TypedStringValue) location).getValue()
                                    : location.toString()) + " to global-properties definition");
                    globalPropertyLocations.add(location);
                }

            }
            // Ensure the bean now references global-properties
            beanProperties.addPropertyValue(LegacyConfigPostProcessor.PROPERTY_PROPERTIES, new RuntimeBeanReference(
                    LegacyConfigPostProcessor.BEAN_NAME_GLOBAL_PROPERTIES));

            // Ensure the new location list is now set on the bean
            if (newLocationList.size() > 0)
            {
                beanProperties.addPropertyValue(LegacyConfigPostProcessor.PROPERTY_LOCATIONS, newLocationList);
            }
            else
            {
                beanProperties.removePropertyValue(LegacyConfigPostProcessor.PROPERTY_LOCATIONS);
            }
        }
        return beanProperties;
    }

    public int getOrder()
    {
        // This has to run before any other post-processor
        return Ordered.HIGHEST_PRECEDENCE;
    }

}
