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

import org.apache.myfaces.orchestra.conversation.basic.LogConversationMessager;
import org.apache.myfaces.orchestra.frameworkAdapter.FrameworkAdapter;
import org.apache.myfaces.orchestra.frameworkAdapter.local.LocalFrameworkAdapter;
import org.springframework.aop.SpringProxy;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.scope.ScopedObject;
import org.springframework.test.AbstractDependencyInjectionSpringContextTests;

/**
 * Test the Conversation.bind method.
 * <p>
 * Normally a bean is associated with a conversation by defining it in Spring and setting its
 * scope to be an appropriate SpringConversationScope object. However that requires the object
 * to be fetched from Spring.
 * <p>
 * However sometimes an instance is created via some other means (eg directly via "new") and we
 * want to force a specific conversation to be entered whenever any of its methods is invoked. The
 * Conversation.bind(obj) method can be used to attach the object to the specified 
 * conversation (which is normally the current conversation). 
 */
public class TestConversationBind extends AbstractDependencyInjectionSpringContextTests
{
    protected String[] getConfigLocations()
    {
        return new String[]
            {
                "classpath:org/apache/myfaces/orchestra/conversation/TestScope.xml"
            };
    }

    protected void onSetUp() throws Exception
    {
        super.onSetUp();
    }

    public static class Dummy
    {
        int count;
        
        public Conversation getConversation()
        {
            return Conversation.getCurrentInstance();
        }
        
        public int getCount()
        {
            return ++count;
        }
    }

    public void testBind() throws Exception
    {
        // Set up the FrameworkAdapter. This is needed by any ConversationManager operation.
        LocalFrameworkAdapter frameworkAdapter = new LocalFrameworkAdapter();
        frameworkAdapter.setApplicationContext(applicationContext);
        frameworkAdapter.setConversationMessager(new LogConversationMessager());
        FrameworkAdapter.setCurrentInstance(frameworkAdapter);

        Dummy dummy = new Dummy();
        assertFalse(dummy instanceof ScopedObject);
        assertFalse(dummy instanceof SpringProxy);
        assertNull(dummy.getConversation());
        
        // Get a conversation-scoped object from Spring. What is returned is actually a
        // trivial "scope proxy" that simply wraps a ScopedBeanTargetSource. 
        //
        // The "scope-proxy" handles interface ScopedObject itself, but otherwise invokes
        // the ScopedBeanTargetSource to get a target object then passes the call on. The
        // "target object" is actually another proxy.
        //
        // The "target-proxy" has advices attached to it that handle interfaces SpringProxy
        // and Advised. It also has attached to it all the advices that were specified on the
        // associated Orchestra scope object, eg CurrentConversationAdvice and (in this
        // test enviroment) MockAdvice.
        //
        // Note that the underlying real target object isn't created until the proxy is forced
        // to fetch its target object.
        final SimpleBean b1 = (SimpleBean) applicationContext.getBean("unscopedBean");
        assertTrue(b1 instanceof ScopedObject); // added by Spring to the scoped-proxy
        assertTrue(b1 instanceof SpringProxy); // added by Spring to the target-proxy
        assertTrue(b1 instanceof Advised); // added by Spring to the target-proxy
        b1.doSomething(); // force conversation to be created.

        // Attach the dummy object to b1's conversation. The way that Conversation.getProxyFor
        // is normally used is that b1 will actually be proxying one of its internal members,
        // not an object passed in.
        Dummy dummyProxy = (Dummy) b1.bindToCurrentConversation(dummy);
        assertTrue(dummyProxy instanceof SpringProxy);
        assertNotNull(dummyProxy.getConversation());
        assertEquals("unscopedBean", dummyProxy.getConversation().getName());

        // Proxy it again, this time from "outside" any conversation context.
        Conversation conv = ConversationManager.getInstance().getConversation("unscopedBean");
        Dummy dummyProxy2 = (Dummy) conv.bind(dummy);
        assertTrue(dummyProxy2 instanceof SpringProxy);
        assertNotSame(dummyProxy, dummyProxy2);
        assertNotNull(dummyProxy2.getConversation());
        assertEquals("unscopedBean", dummyProxy2.getConversation().getName());
    }

    // Ensure that when an arbitrary object is bound to a conversation and the
    // conversation is invalidated, then using the proxy throws an IllegalStateException.
    public void testBindToInvalidatedConversation() throws Exception
    {
        // Set up the FrameworkAdapter. This is needed by any ConversationManager operation.
        LocalFrameworkAdapter frameworkAdapter = new LocalFrameworkAdapter();
        frameworkAdapter.setApplicationContext(applicationContext);
        frameworkAdapter.setConversationMessager(new LogConversationMessager());
        FrameworkAdapter.setCurrentInstance(frameworkAdapter);

        Dummy dummy = new Dummy();

        // force a conversation to be created
        final SimpleBean b1 = (SimpleBean) applicationContext.getBean("unscopedBean");
        b1.doSomething();

        Conversation conv = ConversationManager.getInstance().getConversation("unscopedBean");
        Dummy dummyProxy = (Dummy) conv.bind(dummy);

        // invoke proxy while the conversation is alive: should work
        Conversation convA = dummyProxy.getConversation();
        assertSame(conv, convA);

        // invoke proxy after the conversation is destroyed: should fail
        conv.invalidate();
        assertNull(ConversationManager.getInstance().getConversation("unscopedBean"));
        try
        {
            dummyProxy.getConversation();
            fail("Exception expected but call failed");
        }
        catch(IllegalStateException e)
        {
            // ok, we correctly detected the stale conversation
        }

        // and even if the conversation is recreated, the proxy is still invalid because
        // it references the old conversation.
        b1.doSomething();
        assertNotNull(ConversationManager.getInstance().getConversation("unscopedBean"));
        try
        {
            dummyProxy.getConversation();
            fail("Exception expected but call failed");
        }
        catch(IllegalStateException e)
        {
            // ok, we correctly detected the stale conversation
        }
    }
}
