package com.atlassian.stash.internal.spring;

import com.atlassian.stash.internal.db.DatabaseAffixed;
import com.atlassian.util.concurrent.LazyReference;
import com.atlassian.util.concurrent.ResettableLazyReference;
import com.google.common.base.Throwables;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator;
import org.springframework.jdbc.support.SQLExceptionTranslator;

import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.concurrent.ExecutionException;

/**
 * A {@link DatabaseAffixed} implementation of Spring's {@code SQLExceptionTranslator} which uses a lazy reference
 * to allow recreating the underlying translator when the database is released.
 *
 * @since 3.5
 */
public class DelegatingSqlExceptionTranslator implements DatabaseAffixed, SQLExceptionTranslator {

    private final DataSource dataSource;
    private final ResettableLazyReference<SQLExceptionTranslator> translatorReference =
            new ResettableLazyReference<SQLExceptionTranslator>() {

                @Override
                protected SQLExceptionTranslator create() throws Exception {
                    return new SQLErrorCodeSQLExceptionTranslator(dataSource);
                }
            };

    public DelegatingSqlExceptionTranslator(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    /**
     * Resets the {@link #translatorReference translator reference} to release the old database. Since the
     * reference is lazy, any thread which attempts to translate an exception after this is called will be
     * latched until the new database is available (or the old database is restored).
     * <p>
     * This is something of a compromise, because there isn't really a perfect time to do this work. If we
     * do it here, it means there is a (very small) possibility that an exception thrown by the <i>old</i>
     * database will be translated using the <i>new</i> database's codes. However, if we defer this until
     * the new database is available, we instead create a window where the <i>new</i> database's exceptions
     * get translated with the <i>old</i> database's codes.
     */
    @Override
    public void release() {
        translatorReference.reset();
    }

    @Override
    public DataAccessException translate(String task, String sql, SQLException ex) {
        try {
            return translatorReference.get().translate(task, sql, ex);
        } catch (LazyReference.InitializationException e) {
            Throwable cause = e.getCause();
            if (cause instanceof ExecutionException) {
                cause = cause.getCause();
            }
            Throwables.propagateIfPossible(cause);

            throw e;
        }
    }
}
