/*
 * $Id: WebappMuleXmlConfigurationBuilder.java 11600 2008-04-18 18:13:47Z dfeist $
 * --------------------------------------------------------------------------------------
 * Copyright (c) MuleSource, Inc.  All rights reserved.  http://www.mulesource.com
 *
 * The software in this package is published under the terms of the CPAL v1.0
 * license, a copy of which has been included with this distribution in the
 * LICENSE.txt file.
 */

package org.mule.config.builders;

import org.mule.api.MuleContext;
import org.mule.api.config.ConfigurationException;
import org.mule.api.registry.Registry;
import org.mule.config.ConfigResource;
import org.mule.config.spring.MuleApplicationContext;
import org.mule.config.spring.SpringXmlConfigurationBuilder;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

import javax.servlet.ServletContext;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.access.BeanFactoryLocator;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextException;
import org.springframework.context.access.ContextSingletonBeanFactoryLocator;
import org.springframework.core.io.AbstractResource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.context.ContextLoader;
import org.springframework.web.context.support.ServletContextResource;

/**
 * <code>WebappMuleXmlConfigurationBuilder</code> will first try and load config
 * resources using the ServletContext and if this fails then it will attempt to load
 * config resource from the classpath.
 * <li> ServletContext resources should be relative to the webapp root directory and
 * start with '/'.
 * <li> Classpath resources should be in the webapp classpath and should not start
 * with '/'.
 * 
 * @see org.mule.config.builders.SpringXmlConfigurationBuilder
 */
public class WebappMuleXmlConfigurationBuilder extends SpringXmlConfigurationBuilder
{

    /**
     * Logger used by this class
     */
    protected transient final Log logger = LogFactory.getLog(getClass());

    private ServletContext context;

    public WebappMuleXmlConfigurationBuilder(ServletContext servletContext, String configResources)
        throws ConfigurationException
    {
        super(configResources);
        context = servletContext;
    }

    public WebappMuleXmlConfigurationBuilder(ServletContext servletContext, String[] configResources)
        throws ConfigurationException
    {
        super(configResources);
        context = servletContext;
    }

    protected ConfigResource[] loadConfigResources(String[] configs) throws ConfigurationException
    {
        try
        {
            configResources = new ConfigResource[configs.length];
            for (int i = 0; i < configs.length; i++)
            {
                configResources[i] = new ServletContextOrClassPathConfigResource(configs[i]);
            }
            return configResources;
        }
        catch (IOException e)
        {
            throw new ConfigurationException(e);
        }
    }

    protected void createSpringParentRegistry(MuleContext muleContext, Registry registry, ConfigResource[] all)
    {
        Resource[] servletContextResources = new Resource[all.length];
        for (int i = 0; i < all.length; i++)
        {
            servletContextResources[i] = new ServletContextOrClassPathResource(context, all[i].getResourceName());
        }

        parentContext = loadParentContext(context);

        try
        {
            if (parentContext != null)
            {
                new MuleApplicationContext(muleContext, registry, servletContextResources, parentContext);
            }
            else
            {
                new MuleApplicationContext(muleContext, registry, servletContextResources);
            }
        }
        catch (BeansException e)
        {
            // If creation of MuleApplicationContext fails, remove
            // TransientRegistry->SpringRegistry parent relationship
            registry.setParent(null);
            throw e;
        }
        catch (IOException e)
        {
            registry.setParent(null);
            throw new ApplicationContextException("Failed to load config resource", e);
        }
    }

    /**
     * Used to lookup parent spring ApplicationContext. This allows a parent spring
     * ApplicatonContet to be provided in the same way you would configure a parent
     * ApplicationContext for a spring WebAppplicationContext
     * 
     * @param servletContext
     * @return
     * @throws BeansException
     */
    protected ApplicationContext loadParentContext(ServletContext servletContext) throws BeansException
    {

        ApplicationContext parentContext = null;
        String locatorFactorySelector = servletContext.getInitParameter(ContextLoader.LOCATOR_FACTORY_SELECTOR_PARAM);
        String parentContextKey = servletContext.getInitParameter(ContextLoader.LOCATOR_FACTORY_KEY_PARAM);

        if (parentContextKey != null)
        {
            // locatorFactorySelector may be null, indicating the default
            // "classpath*:beanRefContext.xml"
            BeanFactoryLocator locator = ContextSingletonBeanFactoryLocator.getInstance(locatorFactorySelector);
            if (logger.isDebugEnabled())
            {
                logger.debug("Getting parent context definition: using parent context key of '" + parentContextKey
                             + "' with BeanFactoryLocator");
            }
            parentContext = (ApplicationContext) locator.useBeanFactory(parentContextKey).getFactory();
        }

        return parentContext;
    }

    class ServletContextOrClassPathConfigResource extends ConfigResource
    {
        public ServletContextOrClassPathConfigResource(String resourceName) throws IOException
        {
            super(resourceName, null);
        }

    }

    /**
     * Combines {@link ServletContextResource} and {@link ClassPathResource} to
     * create a {@link Resource} implementation that first tries to load a resource
     * using the {@link ServletContext} and then fails back to use try to load the
     * resource from the classpath.
     */
    class ServletContextOrClassPathResource extends AbstractResource
    {

        private final ServletContext servletContext;

        private final String path;

        public ServletContextOrClassPathResource(ServletContext servletContext, String path)
        {
            Assert.notNull(servletContext, "Cannot resolve ServletContextResource without ServletContext");
            this.servletContext = servletContext;
            // check path
            Assert.notNull(path, "path is required");
            this.path = StringUtils.cleanPath(path);
        }

        public InputStream getInputStream() throws IOException
        {
            InputStream is = getServletContextInputStream();
            if (is == null)
            {
                is = getClasspathInputStream();
            }
            if (is == null)
            {
                throw new FileNotFoundException(getDescription() + " cannot be opened because it does not exist");
            }
            return is;
        }

        protected InputStream getServletContextInputStream()
        {
            String servletContextPath = path;
            if (!servletContextPath.startsWith("/"))
            {
                servletContextPath = "/" + servletContextPath;
            }
            return servletContext.getResourceAsStream(servletContextPath);
        }

        protected InputStream getClasspathInputStream()
        {
            String classpathPath = path;
            if (classpathPath.startsWith("/"))
            {
                classpathPath = classpathPath.substring(1);
            }
            return ClassUtils.getDefaultClassLoader().getResourceAsStream(classpathPath);
        }

        public String getDescription()
        {
            return path;
        }
    }
}
