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

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.TargetSource;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.scope.DefaultScopedObject;
import org.springframework.aop.scope.ScopedObject;
import org.springframework.aop.support.DelegatingIntroductionInterceptor;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;

/**
 * <p>Various Spring utilities used by the conversation framework</p>
 * <p>Notice: this class is not meant to be used outside of this library</p>
 */
public final class _SpringUtils
{
    // Equal to ScopedProxyUtils.TARGET_NAME_PREFIX, but that is private.
    private final static String SCOPED_TARGET_PREFIX = "scopedTarget.";

    private _SpringUtils()
    {
    }

    /**
     * Detect the case where a class includes a nested aop:scoped-target tag.
     * <p>
     * When a definition is present in the config file like this:
     * <pre>
     *  &lt;bean name="foo" class="example.Foo" orchestra:foo="foo"&gt;
     *   &lt;....&gt;
     *   &lt;aop:scopedProxy/&gt;
     *  &lt;/bean&gt;
     * </pre> 
     * then what Spring actually does is create two BeanDefinition objects, one
     * with name "foo" that creates a proxy object, and one with name "scopedTarget.foo"
     * that actually defines the bean of type example.Foo (see Spring class ScopedProxyUtils
     * for details.
     * <p>
     * Using this pattern is very common with Orchestra, so we need to detect it and
     * look for orchestra settings on the renamed bean definition rather than the
     * one with the original name. 
     * 
     * @since 1.1
     */
    public static boolean isModifiedBeanName(String beanName)
    {
        return beanName.startsWith(SCOPED_TARGET_PREFIX);
    }

    /**
     * Given the name of a BeanDefinition, if this is a fake name created
     * by spring because an aop:scoped-proxy is now wrapping this object,
     * then return the name of that scoped-proxy bean (ie the name that the
     * user accesses this bean by).
     * 
     * @since 1.1
     */
    public static String getOriginalBeanName(String beanName)
    {
        if (beanName != null && isModifiedBeanName(beanName))
        {
            return beanName.substring(SCOPED_TARGET_PREFIX.length());
        }

        return beanName;
    }

    /**
     * Given a bean name "foo", if it refers to a scoped proxy then the bean
     * definition that holds the original settings is now under another name,
     * so return that other name so the original BeanDefinition can be found.
     * 
     * @since 1.1
     */
    public static String getModifiedBeanName(String beanName)
    {
        if (beanName != null && !isModifiedBeanName(beanName))
        {
            return SCOPED_TARGET_PREFIX + beanName;
        }

        return beanName;
    }

    /** @deprecated use isModifiedBeanName instead. */
    public static boolean isAlternateBeanName(String beanName) 
    {
        return isModifiedBeanName(beanName);
    }

    /** @deprecated use getModifiedBeanName instead. */
    public static String getAlternateBeanName(String beanName) 
    {
        return getModifiedBeanName(beanName);
    }

    /** @deprecated use getOriginalBeanName instead. */
    public static String getRealBeanName(String beanName) 
    {
        return getOriginalBeanName(beanName);
    }

    /**
     * Create an object that subclasses the concrete class of the BeanDefinition
     * for the specified targetBeanName, and when invoked delegates to an instance
     * of that type fetched from a scope object.
     * <p>
     * The proxy returned currently also currently implements the standard Spring
     * ScopedObject interface; this is experimental and may be removed if not useful.
     * 
     * @since 1.1
     */
    public static Object newProxy(
            AbstractSpringOrchestraScope scope, 
            String conversationName,
            String targetBeanName,
            ObjectFactory objectFactory,
            BeanFactory beanFactory)
    {
        // based on code from  Spring ScopedProxyFactoryBean (APL2.0)
        final Log log = LogFactory.getLog(_SpringUtils.class);

        log.debug("newProxy invoked for " + targetBeanName);

        if (!(beanFactory instanceof ConfigurableBeanFactory))
        {
            throw new IllegalStateException("Not running in a ConfigurableBeanFactory: " + beanFactory);
        }
        ConfigurableBeanFactory cbf = (ConfigurableBeanFactory) beanFactory;

        ScopedBeanTargetSource scopedTargetSource = new ScopedBeanTargetSource(
                scope, conversationName, targetBeanName, objectFactory, beanFactory);

        ProxyFactory pf = new ProxyFactory();
        pf.setProxyTargetClass(true);
        pf.setTargetSource(scopedTargetSource);

        Class beanType = beanFactory.getType(targetBeanName);
        if (beanType == null)
        {
            throw new IllegalStateException("Cannot create scoped proxy for bean '" + targetBeanName +
                    "': Target type could not be determined at the time of proxy creation.");
        }

        // Add an introduction that implements only the methods on ScopedObject.
        // Not sure if this is useful...
        ScopedObject scopedObject = new DefaultScopedObject(cbf, scopedTargetSource.getTargetBeanName());
        pf.addAdvice(new DelegatingIntroductionInterceptor(scopedObject));

        // Add the AopInfrastructureBean marker to indicate that the scoped proxy
        // itself is not subject to auto-proxying! Only its target bean is.
        // Not sure if this is needed....
        pf.addInterface(AopInfrastructureBean.class);

        return pf.getProxy(cbf.getBeanClassLoader());
    }

    /**
     * Given a proxy object, return the underlying object that it currently refers to.
     * <p>
     * This method is currently experimental; it works for the current Spring implementation
     * of Orchestra but at the current time it is not known whether this functionality can
     * be implemented for all dependency-injection frameworks. If it does, then it might
     * later make sense to promote this up to the public Orchestra API.
     * <p>
     * Note that invoking this method will create the "target object" if it does not yet exist.
     * There is currently no way of testing whether this will happen, ie null will never
     * be returned from this method.
     * 
     * @since 1.3
     */
    public static Object getTargetObject(Object proxy) throws Exception
    {
        Advised advised = (Advised) proxy;
        TargetSource targetSource = advised.getTargetSource();
        Object target = targetSource.getTarget();
        
        // Possibly we could add a method on the ScopedBeanTargetSource class to test
        // whether the target bean exists. Then here we could cast TargetSource to
        // ScopedBeanTargetSource and return null if the target does not exist. This
        // might be useful, but let's leave that until someone actually has a use-case
        // for that functionality.
        return target;
    }
}
