/*
 * Copyright 2004-2015 the Seasar Foundation and the Others.
 *
 * Licensed 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.seasar.framework.jpa.impl;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.FlushModeType;
import javax.persistence.LockModeType;
import javax.persistence.Query;
import javax.persistence.TransactionRequiredException;
import javax.transaction.Status;
import javax.transaction.Synchronization;
import javax.transaction.TransactionSynchronizationRegistry;

import org.seasar.framework.container.annotation.tiger.Binding;
import org.seasar.framework.container.annotation.tiger.BindingType;
import org.seasar.framework.container.annotation.tiger.Component;
import org.seasar.framework.jpa.PersistenceUnitManager;
import org.seasar.framework.log.Logger;

/**
 * コンテナ管理{@link javax.persistence.EntityManager}の実装です。
 * <p>
 * このクラスは次のタイプの{@link javax.persistence.EntityManager}となります。
 * <ul>
 * <li>コンテナ管理 (container-managed entity manager)</li>
 * <li>JTAトランザクション (JTA entity manager)</li>
 * <li>トランザクションスコープ (transaction-scoped persistence contexts)</li>
 * </ul>
 * </p>
 * 
 * @author koichik
 */
@Component
public class TxScopedEntityManagerProxy implements EntityManager {

    private static final Logger logger = Logger
            .getLogger(TxScopedEntityManagerProxy.class);

    /** トランザクションシンクロナイゼーションレジストリ */
    @Binding(bindingType = BindingType.MUST)
    protected TransactionSynchronizationRegistry tsr;

    /** 永続マネージャファクトリ */
    @Binding(bindingType = BindingType.MUST)
    protected EntityManagerFactory emf;

    /** 永続ユニットマネージャ */
    @Binding(bindingType = BindingType.MUST)
    protected PersistenceUnitManager pum;

    /**
     * インスタンスを構築します。
     */
    public TxScopedEntityManagerProxy() {
    }

    /**
     * トランザクションが活動中の場合<code>true</code>を返します。
     * 
     * @return トランザクションが活動中の場合<code>true</code>、そうでない場合<code>false</code>
     */
    protected boolean isTxActive() {
        return tsr.getTransactionStatus() != Status.STATUS_NO_TRANSACTION;
    }

    /**
     * トランザクションが活動中であることをアサートします。
     * 
     * @exception TransactionRequiredException
     *                トランザクションが活動中でない場合
     */
    protected void assertTxActive() {
        if (!isTxActive()) {
            throw new TransactionRequiredException();
        }
    }

    /**
     * エンティティマネージャを返します。
     * 
     * @return エンティティマネージャ
     */
    protected EntityManager getEntityManager() {
        if (!isTxActive()) {
            return emf.createEntityManager();
        }
        final EntityManager em = getTxBoundEntityManager();
        if (em != null) {
            return em;
        }
        return createEntityManager();
    }

    /**
     * トランザクションに関連付けられたエンティティマネージャを返します。
     * 
     * @return トランザクションに関連付けられたエンティティマネージャ
     */
    protected EntityManager getTxBoundEntityManager() {
        if (!isTxActive()) {
            return null;
        }
        return EntityManager.class.cast(tsr.getResource(emf));
    }

    /**
     * エンティティマネージャを作成します。
     * 
     * @return エンティティマネージャ
     */
    protected EntityManager createEntityManager() {
        final EntityManager em = emf.createEntityManager();
        tsr.putResource(emf, em);
        tsr.registerInterposedSynchronization(new Synchronization() {

            public void beforeCompletion() {
            }

            public void afterCompletion(final int status) {
                try {
                    em.close();
                } catch (final Throwable t) {
                    logger.log("ESSR0017", new Object[] { t }, t);
                }
            }
        });
        return em;
    }

    public void clear() {
        if (isTxActive()) {
            final EntityManager em = getEntityManager();
            em.clear();
        }
    }

    public void close() {
        throw new IllegalStateException();
    }

    public boolean contains(final Object entity) {
        final boolean mustClose = !isTxActive();
        final EntityManager em = getEntityManager();
        try {
            return em.contains(entity);
        } finally {
            if (mustClose) {
                em.close();
            }
        }
    }

    public Query createNamedQuery(final String name) {
        final boolean mustClose = !isTxActive();
        final EntityManager em = getEntityManager();
        try {
            return em.createNamedQuery(name);
        } finally {
            if (mustClose) {
                em.close();
            }
        }
    }

    public Query createNativeQuery(final String sqlString) {
        final boolean mustClose = !isTxActive();
        final EntityManager em = getEntityManager();
        try {
            return em.createNativeQuery(sqlString);
        } finally {
            if (mustClose) {
                em.close();
            }
        }
    }

    @SuppressWarnings("unchecked")
    public Query createNativeQuery(final String sqlString,
            final Class resultClass) {
        final boolean mustClose = !isTxActive();
        final EntityManager em = getEntityManager();
        try {
            return em.createNativeQuery(sqlString, resultClass);
        } finally {
            if (mustClose) {
                em.close();
            }
        }
    }

    public Query createNativeQuery(final String sqlString,
            final String resultSetMapping) {
        final boolean mustClose = !isTxActive();
        final EntityManager em = getEntityManager();
        try {
            return em.createNativeQuery(sqlString, resultSetMapping);
        } finally {
            if (mustClose) {
                em.close();
            }
        }
    }

    public Query createQuery(final String qlString) {
        final boolean mustClose = !isTxActive();
        final EntityManager em = getEntityManager();
        try {
            return em.createQuery(qlString);
        } finally {
            if (mustClose) {
                em.close();
            }
        }
    }

    public <T> T find(final Class<T> entityClass, final Object primaryKey) {
        final boolean mustClose = !isTxActive();
        final EntityManager em = getEntityManager();
        try {
            return em.find(entityClass, primaryKey);
        } finally {
            if (mustClose) {
                em.close();
            }
        }
    }

    public void flush() {
        if (isTxActive()) {
            final EntityManager em = getEntityManager();
            em.flush();
        }
    }

    public Object getDelegate() {
        final boolean mustClose = !isTxActive();
        final EntityManager em = getEntityManager();
        try {
            return em.getDelegate();
        } finally {
            if (mustClose) {
                em.close();
            }
        }
    }

    public FlushModeType getFlushMode() {
        final boolean mustClose = !isTxActive();
        final EntityManager em = getEntityManager();
        try {
            return em.getFlushMode();
        } finally {
            if (mustClose) {
                em.close();
            }
        }
    }

    public <T> T getReference(final Class<T> entityClass,
            final Object primaryKey) {
        final boolean mustClose = !isTxActive();
        final EntityManager em = getEntityManager();
        try {
            return em.getReference(entityClass, primaryKey);
        } finally {
            if (mustClose) {
                em.close();
            }
        }
    }

    public EntityTransaction getTransaction() {
        throw new IllegalStateException();
    }

    public boolean isOpen() {
        final boolean mustClose = !isTxActive();
        final EntityManager em = getEntityManager();
        try {
            return em.isOpen();
        } finally {
            if (mustClose) {
                em.close();
            }
        }
    }

    public void joinTransaction() {
        assertTxActive();
    }

    public void lock(final Object entity, final LockModeType lockMode) {
        assertTxActive();
        final EntityManager em = getEntityManager();
        em.lock(entity, lockMode);
    }

    public <T> T merge(final T entity) {
        assertTxActive();
        final EntityManager em = getEntityManager();
        return em.merge(entity);
    }

    public void persist(final Object entity) {
        assertTxActive();
        final EntityManager em = getEntityManager();
        em.persist(entity);
    }

    public void refresh(final Object entity) {
        assertTxActive();
        final EntityManager em = getEntityManager();
        em.refresh(entity);
    }

    public void remove(final Object entity) {
        assertTxActive();
        final EntityManager em = getEntityManager();
        em.remove(entity);
    }

    public void setFlushMode(final FlushModeType flushMode) {
        if (isTxActive()) {
            final EntityManager em = getEntityManager();
            em.setFlushMode(flushMode);
        }
    }

}
