/*
 * Copyright 2002-2014 the original author or authors.
 *
 * 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.apache.syncope.core.persistence.jpa.spring;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceException;
import java.sql.Connection;
import org.apache.commons.logging.LogFactory;
import org.apache.openjpa.persistence.FetchPlan;
import org.apache.openjpa.persistence.OpenJPAEntityManager;
import org.apache.openjpa.persistence.OpenJPAPersistence;
import org.apache.openjpa.persistence.jdbc.IsolationLevel;
import org.apache.openjpa.persistence.jdbc.JDBCFetchPlan;
import org.springframework.jdbc.datasource.ConnectionHandle;
import org.springframework.jdbc.datasource.ConnectionHolder;
import org.springframework.jdbc.support.JdbcUtils;
import org.springframework.orm.jpa.DefaultJpaDialect;
import org.springframework.transaction.SavepointManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;

/**
 * {@link org.springframework.orm.jpa.JpaDialect} implementation for Apache OpenJPA.
 * Developed and tested against OpenJPA 2.2.
 *
 * @author Juergen Hoeller
 * @author Costin Leau
 * @since 2.0
 */
public class OpenJpaDialect extends DefaultJpaDialect {

    private static final long serialVersionUID = 2099118508988476959L;

    @Override
    public Object beginTransaction(final EntityManager entityManager, final TransactionDefinition definition)
            throws PersistenceException, TransactionException {

        OpenJPAEntityManager openJpaEntityManager = getOpenJPAEntityManager(entityManager);

        if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) {
            // Pass custom isolation level on to OpenJPA's JDBCFetchPlan configuration
            FetchPlan fetchPlan = openJpaEntityManager.getFetchPlan();
            if (fetchPlan instanceof JDBCFetchPlan jDBCFetchPlan) {
                IsolationLevel isolation = IsolationLevel.fromConnectionConstant(definition.getIsolationLevel());
                jDBCFetchPlan.setIsolation(isolation);
            }
        }

        entityManager.getTransaction().begin();

        if (!definition.isReadOnly()) {
            // Like with EclipseLink, make sure to start the logic transaction early so that other
            // participants using the connection (such as JdbcTemplate) run in a transaction.
            openJpaEntityManager.beginStore();
        }

        // Custom implementation for OpenJPA savepoint handling
        return new OpenJpaTransactionData(openJpaEntityManager);
    }

    @Override
    public ConnectionHandle getJdbcConnection(final EntityManager entityManager, final boolean readOnly)
            throws PersistenceException {

        return new OpenJpaConnectionHandle(getOpenJPAEntityManager(entityManager));
    }

    /**
     * Return the OpenJPA-specific variant of {@code EntityManager}.
     *
     * @param em the generic {@code EntityManager} instance
     * @return the OpenJPA-specific variant of {@code EntityManager}
     */
    protected static OpenJPAEntityManager getOpenJPAEntityManager(final EntityManager em) {
        return OpenJPAPersistence.cast(em);
    }

    /**
     * Transaction data Object exposed from {@code beginTransaction},
     * implementing the {@link SavepointManager} interface.
     */
    private static class OpenJpaTransactionData implements SavepointManager {

        private final OpenJPAEntityManager entityManager;

        private int savepointCounter = 0;

        OpenJpaTransactionData(final OpenJPAEntityManager entityManager) {
            this.entityManager = entityManager;
        }

        @Override
        public Object createSavepoint() throws TransactionException {
            this.savepointCounter++;
            String savepointName = ConnectionHolder.SAVEPOINT_NAME_PREFIX + this.savepointCounter;
            this.entityManager.setSavepoint(savepointName);
            return savepointName;
        }

        @Override
        public void rollbackToSavepoint(final Object savepoint) throws TransactionException {
            this.entityManager.rollbackToSavepoint((String) savepoint);
        }

        @Override
        public void releaseSavepoint(final Object savepoint) throws TransactionException {
            try {
                this.entityManager.releaseSavepoint((String) savepoint);
            } catch (Throwable ex) {
                LogFactory.getLog(OpenJpaTransactionData.class).debug(
                        "Could not explicitly release OpenJPA savepoint", ex);
            }
        }
    }

    /**
     * {@link ConnectionHandle} implementation that fetches a new OpenJPA-provided
     * Connection for every {@code getConnection} call and closes the Connection on
     * {@code releaseConnection}. This is necessary because OpenJPA requires the
     * fetched Connection to be closed before continuing EntityManager work.
     *
     * @see org.apache.openjpa.persistence.OpenJPAEntityManager#getConnection()
     */
    private static class OpenJpaConnectionHandle implements ConnectionHandle {

        private final OpenJPAEntityManager entityManager;

        OpenJpaConnectionHandle(final OpenJPAEntityManager entityManager) {
            this.entityManager = entityManager;
        }

        @Override
        public Connection getConnection() {
            return (Connection) this.entityManager.getConnection();
        }

        @Override
        public void releaseConnection(final Connection con) {
            JdbcUtils.closeConnection(con);
        }
    }
}
