package io.dropwizard.testing.junit;

import java.util.*;
import java.util.function.Supplier;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.cfg.Configuration;
import org.hibernate.context.internal.ManagedSessionContext;
import org.junit.rules.ExternalResource;

import io.dropwizard.logging.BootstrapLogging;

/**
 * A JUnit rule for testing DAOs and Hibernate entities. It allows to quickly
 * test the database access code without starting the Dropwizard infrastructure.
 * <p>
 * Example:
 * <pre><code>
 * {@literal @}Rule
    public DAOTestRule daoTestRule = DAOTestRule.newBuilder()
          .addEntityClass(Person.class)
          .build();

    private PersonDAO personDAO;

   {@literal @}Before
    public void setUp() throws Exception {
        personDAO = new PersonDAO(daoTestRule.getSessionFactory());
    }

   {@literal @}Test
    public void createPerson() {
        Person wizard = daoTestRule.inTransaction(() -> personDAO.create(new Person("Merlin", "The chief wizard")));
        assertThat(wizard.getId()).isGreaterThan(0);
        assertThat(wizard.getFullName()).isEqualTo("Merlin");
        assertThat(wizard.getJobTitle()).isEqualTo("The chief wizard");
    }
 * </code></pre>
 * </p>
 */
public class DAOTestRule extends ExternalResource {

    static {
        BootstrapLogging.bootstrap();
    }

    public static class Builder {

        private String url = "jdbc:h2:mem:" + UUID.randomUUID();
        private String username = "sa";
        private String password = "";
        private String driver = "org.h2.Driver";
        private String hbm2ddlAuto = "create";
        private boolean showSql = false;
        private boolean useSqlComments = false;
        private Set<Class<?>> entityClasses = new LinkedHashSet<>();
        private Map<String, String> properties = new HashMap<>();

        public Builder setUrl(String url) {
            this.url = url;
            return this;
        }

        public Builder setUsername(String username) {
            this.username = username;
            return this;
        }

        public Builder setDriver(Class<? extends java.sql.Driver> driver) {
            this.driver = driver.getName();
            return this;
        }

        public Builder setHbm2DdlAuto(String hbm2ddlAuto) {
            this.hbm2ddlAuto = hbm2ddlAuto;
            return this;
        }

        public Builder setShowSql(boolean showSql) {
            this.showSql = showSql;
            return this;
        }

        public Builder useSqlComments(boolean useSqlComments) {
            this.useSqlComments = useSqlComments;
            return this;
        }

        public Builder addEntityClass(Class<?> entityClass) {
            this.entityClasses.add(entityClass);
            return this;
        }

        public Builder setProperty(String key, String value) {
            this.properties.put(key, value);
            return this;
        }

        public DAOTestRule build() {
            final Configuration config = new Configuration();
            config.setProperty(AvailableSettings.URL, url);
            config.setProperty(AvailableSettings.USER, username);
            config.setProperty(AvailableSettings.PASS, password);
            config.setProperty(AvailableSettings.DRIVER, driver);
            config.setProperty(AvailableSettings.HBM2DDL_AUTO, hbm2ddlAuto);
            config.setProperty(AvailableSettings.SHOW_SQL, String.valueOf(showSql));
            config.setProperty(AvailableSettings.USE_SQL_COMMENTS, String.valueOf(useSqlComments));
            // Use the same configuration as in the Hibernate bundle to reduce differences between
            // testing and production environments.
            config.setProperty(AvailableSettings.CURRENT_SESSION_CONTEXT_CLASS, "managed");
            config.setProperty(AvailableSettings.USE_GET_GENERATED_KEYS, "true");
            config.setProperty(AvailableSettings.GENERATE_STATISTICS, "true");
            config.setProperty(AvailableSettings.USE_REFLECTION_OPTIMIZER, "true");
            config.setProperty(AvailableSettings.ORDER_UPDATES, "true");
            config.setProperty(AvailableSettings.ORDER_INSERTS, "true");
            config.setProperty(AvailableSettings.USE_NEW_ID_GENERATOR_MAPPINGS, "true");
            config.setProperty("jadira.usertype.autoRegisterUserTypes", "true");

            entityClasses.forEach(config::addAnnotatedClass);
            properties.entrySet().forEach(e -> config.setProperty(e.getKey(), e.getValue()));

            return new DAOTestRule(config.buildSessionFactory());
        }
    }

    /**
     * Creates a new builder for {@link DAOTestRule}, which allows to customize a {@link SessionFactory}
     * by different parameters. By default uses the H2 database in the memory mode.
     *
     * @return a new {@link Builder}
     */
    public static Builder newBuilder() {
        return new Builder();
    }

    private final SessionFactory sessionFactory;

    /**
     * Use {@link DAOTestRule#newBuilder()}
     */
    private DAOTestRule(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    @Override
    protected void before() throws Throwable {
        if (ManagedSessionContext.hasBind(sessionFactory)) {
            return;
        }

        final Session session = sessionFactory.openSession();
        ManagedSessionContext.bind(session);
    }

    @Override
    protected void after() {
        if (!ManagedSessionContext.hasBind(sessionFactory)) {
            return;
        }

        final Session currentSession = sessionFactory.getCurrentSession();
        if (currentSession.isOpen()) {
            currentSession.close();
        }
        ManagedSessionContext.unbind(sessionFactory);
    }

    /**
     * Returns the current active session factory for injecting to DAOs.
     *
     * @return {@link SessionFactory} with an open session.
     */
    public SessionFactory getSessionFactory() {
        return sessionFactory;
    }

    /**
     * Performs a call in a transaction
     *
     * @param supplier the call
     * @param <T>  the type of the returned result
     * @return the result of the call
     */
    public <T> T inTransaction(Supplier<T> supplier) {
        final Session session = sessionFactory.getCurrentSession();
        final Transaction transaction = session.beginTransaction();
        try {
            final T result = supplier.get();
            transaction.commit();
            return result;
        } catch (final RuntimeException e) {
            transaction.rollback();
            throw e;
        }
    }

    /**
     * Performs an action in a transaction
     *
     * @param action the action
     */
    public void inTransaction(Runnable action) {
        inTransaction(() -> {
            action.run();
            return true;
        });
    }
}

@@ -42,57 +42,89 @@
 ort 
-java.util.*;%0Aimport java.util.function.Supplier;%0A
+com.google.common.base.Throwables;%0Aimport io.dropwizard.logging.BootstrapLogging;
 %0Aimp
@@ -419,46 +419,57 @@
 ort 
-io.dropwizard.logging.BootstrapLogging
+java.util.*;%0Aimport java.util.concurrent.Callable
 ;%0A%0A/
@@ -6227,24 +6227,20 @@
  @param 
-supplier
+call
  the cal
@@ -6373,28 +6373,24 @@
 ion(
-Supplier%3CT%3E supplier
+Callable%3CT%3E call
 ) %7B%0A
@@ -6572,20 +6572,17 @@
 t = 
-supplier.get
+call.call
 ();%0A
@@ -6669,15 +6669,8 @@
 nal 
-Runtime
 Exce
@@ -6734,17 +6734,39 @@
   throw 
-e
+Throwables.propagate(e)
 ;%0A      

Allow to wrap in a transaction calls which throw checked exceptions