/*
 * 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.springframework.orm.jpa.EntityManagerHolder;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.FlushModeType;
import java.util.Stack;

/**
 * A factory for PersistenceContext objects which integrates with Spring's JPA
 * support.
 * <p>
 * When a bean is invoked which is associated with a conversation, but the conversation
 * does not yet have a PersistenceContext, then this factory is used to create a
 * PersistenceContext.
 * <p>
 * The returned object knows how to configure itself as the "current persistence context"
 * within Spring when a method on that bean is invoked, and how to restore the earlier
 * "current persistence context" after the method returns.
 */
public class JpaPersistenceContextFactory implements PersistenceContextFactory
{
    private EntityManagerFactory entityManagerFactory;

    public PersistenceContext create()
    {
        final EntityManager em = entityManagerFactory.createEntityManager();
        em.setFlushMode(FlushModeType.COMMIT);

        return new PersistenceContext()
        {
            private final Stack bindings = new Stack();

            // Store the current EntityManagerHolder on a stack, then install our own as the
            // current EntityManagerHolder. Storing the old one allows us to restore it when
            // this context is "unbound".
            //
            // Orchestra calls bind every time a method is invoked on a bean which has
            // a conversation with a persistence context. The unbind is invoked when the
            // invoked method on that bean returns.
            //
            // When a bean has a property that has been annotated as @PersistenceContext,
            // Spring injects a proxy that looks up the "current" persistence context whenever
            // a method is invoked on it. Because Orchestra has called persistencecontext.bind
            // when the bean was first entered, and this object's bind method has installed
            // itself as the "current" spring persistence context object, the bean sees the
            // persistence context that is associated with its conversation.
            //
            // TODO: what happens when a bean invokes a method on itself? Does bind get called
            // again? If so, then this implementation is inefficient as it will push itself
            // onto the stack over and over again. This could be optimised by checking whether
            // this is the current context, and if so just incrementing a counter rather than
            // pushing onto a stack...
            public void bind()
            {
                synchronized(bindings)
                {
                    EntityManagerHolder current = (EntityManagerHolder)
                        TransactionSynchronizationManager.getResource(entityManagerFactory);

                    if (current != null)
                    {
                        TransactionSynchronizationManager.unbindResource(entityManagerFactory);
                    }

                    bindings.push(current);

                    TransactionSynchronizationManager.bindResource(entityManagerFactory,
                        new EntityManagerHolder(em));
                }
            }

            public void unbind()
            {
                synchronized(bindings)
                {
                    if (TransactionSynchronizationManager.hasResource(entityManagerFactory))
                    {
                        TransactionSynchronizationManager.unbindResource(entityManagerFactory);
                    }

                    Object holder = null;
                    if (bindings.size() > 0)
                    {
                        holder = bindings.pop();
                    }
                    if (holder != null)
                    {
                        TransactionSynchronizationManager.bindResource(entityManagerFactory,
                            holder);
                    }
                }
            }

            public void close()
            {
                em.close();
            }
        };
    }

    public EntityManagerFactory getEntityManagerFactory()
    {
        return entityManagerFactory;
    }

    public void setEntityManagerFactory(EntityManagerFactory entityManagerFactory)
    {
        this.entityManagerFactory = entityManagerFactory;
    }
}
