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

package org.apache.myfaces.orchestra.viewController.spring;

import org.apache.myfaces.orchestra.conversation.Conversation;
import org.apache.myfaces.orchestra.conversation.ConversationContext;
import org.apache.myfaces.orchestra.conversation.spring.AbstractSpringOrchestraScope;
import org.apache.myfaces.orchestra.frameworkAdapter.FrameworkAdapter;
import org.apache.myfaces.orchestra.lib.OrchestraException;
import org.apache.myfaces.orchestra.viewController.DefaultViewControllerManager;
import org.apache.myfaces.orchestra.viewController.ViewControllerManager;
import org.springframework.aop.scope.ScopedProxyFactoryBean;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.context.ConfigurableApplicationContext;

/**
 * This hooks into the Spring2.x scope-handling mechanism to provide a dummy scope type
 * which will place a bean configured for it into the same conversation that the current
 * viewController lives in.
 * <p>
 * To use this, in the spring config file add an element with id=N that defines a spring
 * scope with this class as its controller. Then define managed beans with scope="N". When
 * code references such a bean, then the current "view controller" bean is located
 * (ie the bean handling lifecycle events for the current view), and the instance of
 * the target bean from the same conversation is returned. If no such instance currently
 * exists, then one is created and added to that conversation (even when an instance
 * already exists in a different scope).
 * <p>
 * Note that this means a bean configured with a scope of this type will actually
 * have a separate instance per conversation.
 * <p>
 * In particular, this works well with spring aop-proxy, where the proxy looks up the
 * bean on each method call, and so always returns the instance in the conversation
 * associated with the current view.
 * <p>
 * One use for this is implementing custom JSF converters or validators that access
 * persistent objects. When accessing the database they need to use the same
 * PersistenceContext that the beans handing this view use. Defining the converter
 * using this scope type ensures that this happens.
 * <p>
 * It is an error (ie an exception is thrown) if a bean of this scope is referenced
 * but there is no "view controller" bean associated with the current view.
 */
public class SpringViewControllerScope extends AbstractSpringOrchestraScope
{
    // should this really be static? Are all the classes it references really static?
    private final static ViewControllerManager DEFAULT_VCM = new DefaultViewControllerManager();

    public SpringViewControllerScope()
    {
    }

    public Conversation createConversation(ConversationContext context, String conversationName)
    {
        throw new IllegalStateException(
            "The viewController scope is not supposed to start a conversation on its own. " +
            "Conversation to start: " + conversationName);
    }

    protected void assertSameScope(String beanName, Conversation conversation)
    {
        // since we do not start any conversation, there is no need to check
        // if this scope uses the same conversationFactory
    }

    /**
     * Find the conversation-controller bean for the current view, then return the conversation that
     * is configured for that controller bean.
     * <p>
     * The parameter is completely ignored; the conversation-name returned is that associated with the
     * controller bean, not the specified bean at all.
     */
    public String getConversationNameForBean(String beanName)
    {
        ViewControllerManager viewControllerManager = getViewControllerManager();
        String viewId = FrameworkAdapter.getCurrentInstance().getCurrentViewId();
        String viewControllerName = viewControllerManager.getViewControllerName(viewId);
        if (viewControllerName == null)
        {
            // The current view does not have any bean that is its "view controller", ie
            // which handles the lifecycle events for that view. Therefore we cannot 
            // do anything more here...
            throw new OrchestraException(
                "Error while processing bean " + beanName
                + ": no view controller name found for view " + viewId);
        }

        // Look up the definition with the specified name.
        ConfigurableApplicationContext appContext = getApplicationContext();
        ConfigurableListableBeanFactory beanFactory = appContext.getBeanFactory();
        BeanDefinition beanDefinition = beanFactory.getBeanDefinition(viewControllerName);

        if (beanDefinition.getBeanClassName().equals(ScopedProxyFactoryBean.class.getName()))
        {
            // The BeanDefinition we found is one that contains a nested aop:scopedProxy tag.
            // In this case, Spring has actually renamed the original BeanDefinition which
            // contains the data we need. So here we need to look up the renamed definition.
            //
            // It would be nice if the fake definition that Spring creates had some way of
            // fetching the definition it wraps, but instead it appears that we need to
            // rely on the magic string prefix...
            beanDefinition = beanFactory.getBeanDefinition("scopedTarget." + viewControllerName);
        }

        String scopeName = beanDefinition.getScope();
        
        if (scopeName == null)
        {
            // should never happen
            throw new OrchestraException(
                "Error while processing bean " + beanName
                + ": view controller " + viewControllerName + " has no scope."); 
        }

        Scope registeredScope = beanFactory.getRegisteredScope(scopeName);
        if (registeredScope == null)
        {
            throw new OrchestraException(
                "Error while processing bean " + beanName
                + ": view controller " + viewControllerName 
                + " has unknown scope " + scopeName); 
        }

        if (registeredScope instanceof AbstractSpringOrchestraScope)
        {
            return ((AbstractSpringOrchestraScope) registeredScope).getConversationNameForBean(viewControllerName);
        }

        throw new OrchestraException(
            "Error while processing bean " + beanName
            + ": the scope " + scopeName
            + " should be of type AbstractSpringOrchestraScope"
            + ", but is type " + registeredScope.getClass().getName());
    }

    /**
     * Look for a Spring bean definition that defines a custom ViewControllerManager;
     * if not found then return the default instance.
     * <p>
     * Never returns null.
     */
    private ViewControllerManager getViewControllerManager()
    {
        try
        {
            return (ViewControllerManager)
                getApplicationContext().getBean(
                    ViewControllerManager.VIEW_CONTROLLER_MANAGER_NAME,
                    ViewControllerManager.class);
        }
        catch(NoSuchBeanDefinitionException e)
        {
            return DEFAULT_VCM;
        }
    }
}