to.etc.domui.hibernate.config.HibernateConfigurator.java Source code

Java tutorial

Introduction

Here is the source code for to.etc.domui.hibernate.config.HibernateConfigurator.java

Source

/*
 * DomUI Java User Interface library
 * Copyright (c) 2010 by Frits Jalvingh, Itris B.V.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * See the "sponsors" file for a list of supporters.
 *
 * The latest version of DomUI and related code, support and documentation
 * can be found at http://www.domui.org/
 * The contact for the project is Frits Jalvingh <jal@etc.to>.
 */
package to.etc.domui.hibernate.config;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.boot.Metadata;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.SessionFactoryBuilder;
import org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl;
import org.hibernate.boot.registry.BootstrapServiceRegistry;
import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.EventType;
import org.hibernate.jpa.event.spi.JpaIntegrator;
import org.hibernate.service.ServiceRegistry;
import to.etc.dbpool.ConnectionPool;
import to.etc.dbpool.PoolManager;
import to.etc.domui.hibernate.beforeimages.BeforeImageInterceptor;
import to.etc.domui.hibernate.beforeimages.CopyCollectionEventListener;
import to.etc.domui.hibernate.beforeimages.CreateBeforeImagePostLoadListener;
import to.etc.domui.hibernate.generic.BuggyHibernateBaseContext;
import to.etc.domui.hibernate.generic.HibernateLongSessionContextFactory;
import to.etc.domui.hibernate.generic.HibernateQueryExecutor;
import to.etc.domui.hibernate.generic.HibernateSessionMaker;
import to.etc.util.DeveloperOptions;
import to.etc.util.StringTool;
import to.etc.webapp.qsql.JdbcQueryExecutor;
import to.etc.webapp.query.IQueryExecutorFactory;
import to.etc.webapp.query.IQueryListener;
import to.etc.webapp.query.QCriteria;
import to.etc.webapp.query.QDataContext;
import to.etc.webapp.query.QDataContextFactory;
import to.etc.webapp.query.QEventListenerSet;
import to.etc.webapp.query.QQueryExecutorRegistry;

import javax.sql.DataSource;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Helper class to help with configuring Hibernate for DomUI easily. You are not required to
 * use this class at all; "normal" hibernate configuration works as "well" (meh), but this
 * tries to hide lots of boilerplate needed to configure properly while only exposing that
 * which is really needed in most cases.
 *
 * @author <a href="mailto:jal@etc.to">Frits Jalvingh</a>
 * Created on Dec 30, 2010
 */
@NonNullByDefault
final public class HibernateConfigurator {
    /** This is the DataSource that will provide all Hibernate connections for us. */
    static private DataSource m_dataSource;

    /** The session factory created after initialization. */
    static private SessionFactory m_sessionFactory;

    /** The DomUI Context source */
    static private QDataContextFactory m_contextSource;

    /** All classes registered as part of the config. */
    static final private List<Class<?>> m_annotatedClassList = new ArrayList<Class<?>>();

    /** When non-null, the user has set the "show sql" option. When unset it defaults to the DeveloperOptions setting. */
    static private Boolean m_showSQL;

    /** The event listener set defined for DomUI. */
    static private QEventListenerSet m_listeners = new QEventListenerSet();

    /** The registered query handlers for DomUI */
    static private QQueryExecutorRegistry m_handlers = new QQueryExecutorRegistry();

    static private List<IHibernateConfigListener> m_onConfigureList = Collections.emptyList();

    private static boolean m_allowHibernateHiloSequences;

    private final static Map<String, String> m_hibernateOptions = new HashMap<>();

    /**
     * Defines the database update mode (hibernate.hbm2ddl.auto).
     *
     * @author <a href="mailto:jal@etc.to">Frits Jalvingh</a>
     * Created on Dec 30, 2010
     */
    public enum Mode {
        /**
         * Assume the database is correct and the same as the Hibernate expected schema.
         */
        NONE

        /**
         * Check table definitions and alter the database to correspond to the Hibernate schema as much as possible.
         */
        , UPDATE

        /**
         * DANGEROUS: drop the entire database AND ITS DATA, and recreate all tables.
         */
        ,CREATE
    }

    /**
     * The database creation/update mode.
     */
    static private Mode m_mode = Mode.NONE;

    static private boolean m_observableEnabled;

    static private boolean m_beforeImagesEnabled;

    public static List<Class<?>> getAnnotatedClassList() {
        return m_annotatedClassList;
    }

    /*--------------------------------------------------------------*/
    /*   CODING:   Accessing the completed configuration's data.      */
    /*--------------------------------------------------------------*/

    static public void setHibernateOption(String option, String value) {
        configured();
        m_hibernateOptions.put(option, value);
    }

    /**
     * Return the datasource, as configured.
     */
    @NonNull
    static synchronized public DataSource getDataSource() {
        if (null == m_dataSource)
            throw new IllegalStateException(
                    "The initialize() method has not yet been called with a valid datasource");
        return m_dataSource;
    }

    /**
     * Return the Hibernate SessionFactory created by this code. Should not normally be used by common user code.
     */
    public synchronized static SessionFactory getSessionFactory() {
        unconfigured();
        return m_sessionFactory;
    }

    /**
     * Unwrap the QDataContext and obtain it's Hibernate {@link Session} record.
     */
    public static Session internalGetSession(final QDataContext dc) throws Exception {
        return ((BuggyHibernateBaseContext) dc).getSession();
    }

    /**
     * Returns the data context factory wrapping the hibernate code.
     */
    public synchronized static QDataContextFactory getDataContextFactory() {
        unconfigured();
        return m_contextSource;
    }

    /*--------------------------------------------------------------*/
    /*   CODING:   Configuration setters.                        */
    /*--------------------------------------------------------------*/

    /**
     * Abort if initialize() has already completed.
     */
    static synchronized private void configured() {
        if (null != m_sessionFactory)
            throw new IllegalStateException(
                    "This method must be called BEFORE one of the 'initialize' methods gets called.");
    }

    /**
     * Abort if we have not yet initialize()d successfully.
     */
    static synchronized private void unconfigured() {
        if (null == m_sessionFactory)
            throw new IllegalStateException(
                    "This method must be called AFTER one of the 'initialize' methods gets called.");
    }

    static public synchronized void addConfigListener(IHibernateConfigListener listener) {
        m_onConfigureList = new ArrayList<>(m_onConfigureList);
        m_onConfigureList.add(listener);
    }

    /**
     * Must be called before one of the "initialize" methods gets called, to register
     * all POJO classes that need to be configured with Hibernate. The classes will
     * be added to the AnnotationConfiguration for Hibernate when initialize() is
     * called. You can call this as many times as needed; all classes are <i>added</i> to
     * a list.
     */
    static public void addClasses(Class<?>... classes) {
        configured();
        for (Class<?> clz : classes)
            m_annotatedClassList.add(clz);
    }

    /**
     * Set the "show sql" setting for hibernate. When called it overrides any "developer.properties" setting.
     */
    static public void showSQL(boolean on) {
        configured();
        m_showSQL = Boolean.valueOf(on);
    }

    /**
     * Set the "schema update" mode for Hibernate (corresponding to hbm2ddl.auto). It defaults to NONE. When
     * set to UPDATE Hibernate will do it's best to change the database schema in such a way that it corresponds
     * to the annotated classes' definition.
     */
    static public void schemaUpdate(@NonNull Mode m) {
        configured();
        m_mode = m;
    }

    /**
     * Register a DomUI {@link IQueryListener} that will be called when DomUI executes {@link QCriteria} queries.
     */
    static public void registerQueryListener(IQueryListener ql) {
        configured();
        m_listeners.addQueryListener(ql);
    }

    /**
     * Register an alternative {@link IQueryExecutorFactory} which can execute QCriteria queries on non-hibernate
     * classes. <b>WARNING</b>: if
     * you use this call <i>all default query executors are not registered</i>. This allows you to override them if needed. It also
     * means that <i>no Hibernate QCriteria code works at all</i> if you do not add them! The default factories should be registered
     * as follows:
     * <pre>
     * registerQueryListener(JdbcQueryExecutor.FACTORY);
     * registerQueryListener(HibernateQueryExecutor.FACTORY);
     * </pre>
     * By ordering your executors with the default ones you can control the order of acceptance for queries.
     */
    static public void registerQueryExecutor(IQueryExecutorFactory qexecutor) {
        configured();
        m_handlers.register(qexecutor);
    }

    static public void enableBeforeImages(boolean yes) {
        configured();
        m_beforeImagesEnabled = yes;
    }

    static public void enableObservableCollections(boolean yes) {
        configured();
        m_observableEnabled = yes;
    }

    static private void enhanceMappings(@NonNull Metadata metaData) throws Exception {
        HibernateChecker hc = new HibernateChecker(metaData, DeveloperOptions.isDeveloperWorkstation(),
                m_observableEnabled, m_allowHibernateHiloSequences);
        hc.enhanceMappings();
    }

    /*--------------------------------------------------------------*/
    /*   CODING:   Main initialization entrypoints.               */
    /*--------------------------------------------------------------*/

    /**
     * Main worker to initialize the database layer, using Hibernate, with a user-specified core data source. This
     * code also enables SQL logging when .developer.properties option hibernate.sql=true.
     */
    public synchronized static void initialize(final DataSource ds) throws Exception {
        System.setProperty("org.jboss.logging.provider", "slf4j"); // Thanks to https://stackoverflow.com/questions/11639997/how-do-you-configure-logging-in-hibernate-4-to-use-slf4j
        if (m_sessionFactory != null)
            throw new IllegalStateException("HibernateConfigurator has already been initialized!");
        if (m_annotatedClassList.size() == 0)
            throw new IllegalStateException(
                    "Please call addClasses(Class<?>...) and register your Hibernate data classes before calling me.");

        long ts = System.nanoTime();
        m_dataSource = ds;

        // see https://www.boraji.com/hibernate-5-event-listener-example

        //-- Create Hibernate's config. See https://docs.jboss.org/hibernate/orm/5.1/userguide/html_single/chapters/bootstrap/Bootstrap.html
        /*
         * Hibernate apparently cannot initialize without the useless hibernate.cfg.xml file. We cannot
         * add that file at the root location because that would interfere with applications. To have a
         * working model we add it as a resource in this class's package. And of course Hibernate makes
         * it hard to reach- we need to calculate the proper name, sigh.
         */
        BootstrapServiceRegistry bootstrapRegistry = new BootstrapServiceRegistryBuilder()
                .applyIntegrator(new JpaIntegrator()).build();

        String resname = "/" + HibernateConfigurator.class.getPackage().getName().replace('.', '/')
                + "/hibernate.cfg.xml";
        StandardServiceRegistryBuilder serviceBuilder = new StandardServiceRegistryBuilder(bootstrapRegistry)
                .configure(resname);

        /*
         * Set other properties according to config settings made.
         */
        serviceBuilder.applySetting("hibernate.connection.datasource", ds);
        boolean logsql;
        if (m_showSQL == null)
            logsql = DeveloperOptions.getBool("hibernate.sql", false); // Take default from .developer.properties
        else
            logsql = m_showSQL.booleanValue();

        if (logsql) {
            serviceBuilder.applySetting("show_sql", "true");
            serviceBuilder.applySetting("hibernate.show_sql", "true");
        }

        /*
         * Hibernate defaults to completely non-standard behavior for sequences, using the
         * "hilo" sequence generator by default. This irresponsible behavior means that
         * by default Hibernate code is incompatible with any code using sequences.
         * Since that is irresponsible and downright DUMB this reverts the behavior to
         * using sequences in their normal behavior.
         * See https://stackoverflow.com/questions/12745751/hibernate-sequencegenerator-and-allocationsize
         */
        serviceBuilder.applySetting("hibernate.id.new_generator_mappings", "true"); // MUST BE BEFORE config.configure

        m_hibernateOptions.forEach((option, value) -> serviceBuilder.applySetting(option, value));

        if (DeveloperOptions.getBool("hibernate.format_sql", true)) {
            serviceBuilder.applySetting("hibernate.format_sql", "true");
        }

        switch (m_mode) {
        default:
            throw new IllegalStateException("Mode: " + m_mode);
        case CREATE:
            serviceBuilder.applySetting("hbm2ddl.auto", "create");
            serviceBuilder.applySetting("hibernate.hbm2ddl.auto", "create");
            break;
        case NONE:
            serviceBuilder.applySetting("hbm2ddl.auto", "none");
            serviceBuilder.applySetting("hibernate.hbm2ddl.auto", "none");
            break;
        case UPDATE:
            serviceBuilder.applySetting("hbm2ddl.auto", "update");
            serviceBuilder.applySetting("hibernate.hbm2ddl.auto", "update");
            break;
        }

        // change settings
        for (IHibernateConfigListener listener : m_onConfigureList) {
            listener.onSettings(serviceBuilder);
        }

        ServiceRegistry reg = serviceBuilder.build();
        MetadataSources sources = new MetadataSources(reg);

        for (Class<?> clz : m_annotatedClassList)
            sources.addAnnotatedClass(clz);

        // add classes
        for (IHibernateConfigListener listener : m_onConfigureList) {
            listener.onAddSources(sources);
        }

        Metadata metaData = sources.getMetadataBuilder()
                .applyImplicitNamingStrategy(ImplicitNamingStrategyJpaCompliantImpl.INSTANCE).build();

        enhanceMappings(metaData);

        //for(Consumer<Configuration> listener : m_onConfigureList) {
        //   listener.accept(config);
        //}

        //-- Create the session factory: this completes the Hibernate config part.
        SessionFactoryBuilder sessionFactoryBuilder = metaData.getSessionFactoryBuilder();

        //      sessionFactoryBuilder.applyInterceptor( new CustomSessionFactoryInterceptor() );

        //sessionFactoryBuilder.addSessionFactoryObservers( new CustomSessionFactoryObserver() );

        // Apply a CDI BeanManager ( for JPA event listeners )
        //sessionFactoryBuilder.applyBeanManager( getBeanManager() );

        SessionFactoryImplementor sessionFactory = (SessionFactoryImplementor) sessionFactoryBuilder.build();
        m_sessionFactory = sessionFactory;

        EventListenerRegistry listenerRegistry = sessionFactory.getServiceRegistry()
                .getService(EventListenerRegistry.class);
        if (m_beforeImagesEnabled) {
            // https://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/chapters/events/Events.html
            listenerRegistry.prependListeners(EventType.POST_LOAD, new CreateBeforeImagePostLoadListener());
            listenerRegistry.prependListeners(EventType.INIT_COLLECTION, new CopyCollectionEventListener());
        }
        for (IHibernateConfigListener listener : m_onConfigureList) {
            listener.onAddListeners(listenerRegistry);
        }

        //-- Start DomUI/WebApp.core initialization: generalized database layer
        HibernateSessionMaker hsm;
        if (m_beforeImagesEnabled) {
            //-- We need the copy interceptor to handle these.
            hsm = dc -> {
                return m_sessionFactory.withOptions().interceptor(new BeforeImageInterceptor(dc.getBeforeCache()))
                        .openSession();
                //return m_sessionFactory.openSession(new BeforeImageInterceptor(dc.getBeforeCache()));
            };
        } else {
            hsm = dc -> m_sessionFactory.openSession();
        }

        //-- If no handlers are registered: register the default ones.
        if (m_handlers.size() == 0) {
            m_handlers.register(JdbcQueryExecutor.FACTORY);
            m_handlers.register(HibernateQueryExecutor.FACTORY);
        }

        m_contextSource = new HibernateLongSessionContextFactory(m_listeners, hsm, m_handlers);
        System.out.println("domui: Hibernate initialization took a whopping "
                + StringTool.strNanoTime(System.nanoTime() - ts));
    }

    /**
     * Alternate entrypoint: initialize the layer using a poolID in the default poolfile.
     */
    public static void initialize(final String poolname) throws Exception {
        ConnectionPool p = PoolManager.getInstance().definePool(poolname);
        initialize(p.getPooledDataSource());
    }

    /**
     * Initialize the layer using a poolid in the specified poolfile.
     */
    public static void initialize(final File poolfile, final String poolname) throws Exception {
        ConnectionPool p = PoolManager.getInstance().definePool(poolfile, poolname);
        initialize(p.getPooledDataSource());
    }

    /**
     * The configurator forces hibernate to obey sequence rules proper so that
     * interaction with existing application just works. When you are sure that
     * no other applications update the database or if all of those also use
     * the same hilo mechanism setting this to TRUE will greatly increase insert
     * performance.
     */
    public static void setAllowHiloSequences(boolean allowHibernateSuckySequences) {
        m_allowHibernateHiloSequences = allowHibernateSuckySequences;
    }
}