/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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
 *
 *    https://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.grails.orm.hibernate;

import java.io.IOException;
import java.io.Serializable;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;

import javax.sql.DataSource;

import org.hibernate.SessionFactory;
import org.hibernate.boot.Metadata;
import org.hibernate.boot.SchemaAutoTooling;
import org.hibernate.cfg.Environment;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.integrator.spi.Integrator;
import org.hibernate.integrator.spi.IntegratorService;
import org.hibernate.service.ServiceRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.StaticMessageSource;
import org.springframework.core.env.PropertyResolver;
import org.springframework.jdbc.datasource.ConnectionHolder;
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import grails.gorm.MultiTenant;
import org.grails.datastore.gorm.events.AutoTimestampEventListener;
import org.grails.datastore.gorm.events.ConfigurableApplicationContextEventPublisher;
import org.grails.datastore.gorm.events.ConfigurableApplicationEventPublisher;
import org.grails.datastore.gorm.events.DefaultApplicationEventPublisher;
import org.grails.datastore.gorm.jdbc.MultiTenantConnection;
import org.grails.datastore.gorm.jdbc.MultiTenantDataSource;
import org.grails.datastore.gorm.jdbc.connections.DataSourceConnectionSource;
import org.grails.datastore.gorm.jdbc.connections.DataSourceConnectionSourceFactory;
import org.grails.datastore.gorm.jdbc.connections.DataSourceSettings;
import org.grails.datastore.gorm.utils.ClasspathEntityScanner;
import org.grails.datastore.gorm.validation.constraints.MappingContextAwareConstraintFactory;
import org.grails.datastore.gorm.validation.constraints.builtin.UniqueConstraint;
import org.grails.datastore.gorm.validation.constraints.registry.ConstraintRegistry;
import org.grails.datastore.mapping.core.ConnectionNotFoundException;
import org.grails.datastore.mapping.core.Datastore;
import org.grails.datastore.mapping.core.DatastoreUtils;
import org.grails.datastore.mapping.core.Session;
import org.grails.datastore.mapping.core.connections.ConnectionSource;
import org.grails.datastore.mapping.core.connections.ConnectionSources;
import org.grails.datastore.mapping.core.connections.ConnectionSourcesInitializer;
import org.grails.datastore.mapping.core.connections.DefaultConnectionSource;
import org.grails.datastore.mapping.core.connections.SingletonConnectionSources;
import org.grails.datastore.mapping.core.exceptions.ConfigurationException;
import org.grails.datastore.mapping.engine.event.DatastoreInitializedEvent;
import org.grails.datastore.mapping.model.DatastoreConfigurationException;
import org.grails.datastore.mapping.model.MappingContext;
import org.grails.datastore.mapping.model.PersistentEntity;
import org.grails.datastore.mapping.multitenancy.AllTenantsResolver;
import org.grails.datastore.mapping.multitenancy.MultiTenancySettings;
import org.grails.datastore.mapping.validation.ValidatorRegistry;
import org.grails.orm.hibernate.cfg.GrailsDomainBinder;
import org.grails.orm.hibernate.cfg.HibernateMappingContext;
import org.grails.orm.hibernate.cfg.Settings;
import org.grails.orm.hibernate.connections.HibernateConnectionSource;
import org.grails.orm.hibernate.connections.HibernateConnectionSourceFactory;
import org.grails.orm.hibernate.connections.HibernateConnectionSourceSettings;
import org.grails.orm.hibernate.event.listener.HibernateEventListener;
import org.grails.orm.hibernate.multitenancy.MultiTenantEventListener;
import org.grails.orm.hibernate.support.ClosureEventTriggeringInterceptor;

/**
 * Datastore implementation that uses a Hibernate SessionFactory underneath.
 *
 * @author Graeme Rocher
 * @since 2.0
 */
public class HibernateDatastore extends AbstractHibernateDatastore implements MessageSourceAware {
    private static final Logger LOG = LoggerFactory.getLogger(HibernateDatastore.class);

    protected final GrailsHibernateTransactionManager transactionManager;
    protected ConfigurableApplicationEventPublisher eventPublisher;
    protected final HibernateGormEnhancer gormEnhancer;
    protected final Map<String, HibernateDatastore> datastoresByConnectionSource = new LinkedHashMap<>();
    protected final Metadata metadata;

    /**
     * Create a new HibernateDatastore for the given connection sources and mapping context
     *
     * @param connectionSources The {@link ConnectionSources} instance
     * @param mappingContext The {@link MappingContext} instance
     * @param eventPublisher The {@link ConfigurableApplicationEventPublisher} instance
     */
    public HibernateDatastore(final ConnectionSources<SessionFactory, HibernateConnectionSourceSettings> connectionSources, final HibernateMappingContext mappingContext, final ConfigurableApplicationEventPublisher eventPublisher) {
        super(connectionSources, mappingContext);

        this.metadata = getMetadataInternal();

        HibernateConnectionSource defaultConnectionSource = (HibernateConnectionSource) connectionSources.getDefaultConnectionSource();
        this.transactionManager = new GrailsHibernateTransactionManager(
                                                                        defaultConnectionSource.getSource(),
                                                                        defaultConnectionSource.getDataSource(),
                                                                        org.hibernate.FlushMode.valueOf(defaultFlushModeName));
        this.eventPublisher = eventPublisher;
        this.eventTriggeringInterceptor = new HibernateEventListener(this);
        this.autoTimestampEventListener = new AutoTimestampEventListener(this);

        HibernateConnectionSourceSettings settings = defaultConnectionSource.getSettings();
        HibernateConnectionSourceSettings.HibernateSettings hibernateSettings = settings.getHibernate();

        ClosureEventTriggeringInterceptor interceptor = (ClosureEventTriggeringInterceptor) hibernateSettings.getEventTriggeringInterceptor();
        interceptor.setDatastore(this);
        interceptor.setEventPublisher(eventPublisher);
        registerEventListeners(this.eventPublisher);
        configureValidatorRegistry(settings, mappingContext);
        this.mappingContext.addMappingContextListener(new MappingContext.Listener() {
            @Override
            public void persistentEntityAdded(PersistentEntity entity) {
                gormEnhancer.registerEntity(entity);
            }
        });
        initializeConverters(this.mappingContext);

        if (!(connectionSources instanceof SingletonConnectionSources)) {

            final HibernateDatastore parent = this;
            Iterable<ConnectionSource<SessionFactory, HibernateConnectionSourceSettings>> allConnectionSources = connectionSources.getAllConnectionSources();
            for (ConnectionSource<SessionFactory, HibernateConnectionSourceSettings> connectionSource : allConnectionSources) {
                SingletonConnectionSources<SessionFactory, HibernateConnectionSourceSettings> singletonConnectionSources = new SingletonConnectionSources<>(connectionSource, connectionSources.getBaseConfiguration());
                HibernateDatastore childDatastore;

                if (ConnectionSource.DEFAULT.equals(connectionSource.getName())) {
                    childDatastore = this;
                } else {
                    childDatastore = createChildDatastore(mappingContext, eventPublisher, parent, singletonConnectionSources);
                }
                datastoresByConnectionSource.put(connectionSource.getName(), childDatastore);
            }

            // register a listener to update the datastore each time a connection source is added at runtime
            connectionSources.addListener(connectionSource -> {
                SingletonConnectionSources<SessionFactory, HibernateConnectionSourceSettings> singletonConnectionSources = new SingletonConnectionSources<>(connectionSource, connectionSources.getBaseConfiguration());
                HibernateDatastore childDatastore = createChildDatastore(mappingContext, eventPublisher, parent, singletonConnectionSources);
                datastoresByConnectionSource.put(connectionSource.getName(), childDatastore);
                registerAllEntitiesWithEnhancer();
            });

            if (multiTenantMode == MultiTenancySettings.MultiTenancyMode.SCHEMA) {
                if (this.tenantResolver instanceof AllTenantsResolver) {
                    AllTenantsResolver allTenantsResolver = (AllTenantsResolver) tenantResolver;
                    Iterable<Serializable> tenantIds = allTenantsResolver.resolveTenantIds();

                    for (Serializable tenantId : tenantIds) {
                        addTenantForSchemaInternal(tenantId.toString());
                    }
                }
                else {
                    Collection<String> allSchemas = schemaHandler.resolveSchemaNames(defaultConnectionSource.getDataSource());
                    for (String schema : allSchemas) {
                        addTenantForSchemaInternal(schema);
                    }
                }
            }
        }

        this.gormEnhancer = initialize();
    }

    private HibernateDatastore createChildDatastore(HibernateMappingContext mappingContext,
                                                    ConfigurableApplicationEventPublisher eventPublisher,
                                                    HibernateDatastore parent,
                                                    SingletonConnectionSources<SessionFactory, HibernateConnectionSourceSettings> singletonConnectionSources) {
        return new HibernateDatastore(singletonConnectionSources, mappingContext, eventPublisher) {
            @Override
            protected HibernateGormEnhancer initialize() {
                return null;
            }

            @Override
            public HibernateDatastore getDatastoreForConnection(String connectionName) {
                if (connectionName.equals(Settings.SETTING_DATASOURCE) || connectionName.equals(ConnectionSource.DEFAULT)) {
                    return parent;
                } else {
                    HibernateDatastore hibernateDatastore = parent.datastoresByConnectionSource.get(connectionName);
                    if (hibernateDatastore == null) {
                        throw new ConfigurationException("DataSource not found for name [" + connectionName + "] in configuration. Please check your multiple data sources configuration and try again.");
                    }
                    return hibernateDatastore;
                }
            }
        };
    }

    /**
     * Create a new HibernateDatastore for the given connection sources and mapping context
     *
     * @param configuration The configuration
     * @param connectionSourceFactory The {@link HibernateConnectionSourceFactory} instance
     * @param eventPublisher The {@link ConfigurableApplicationEventPublisher} instance
     */
    public HibernateDatastore(PropertyResolver configuration, HibernateConnectionSourceFactory connectionSourceFactory, ConfigurableApplicationEventPublisher eventPublisher) {
        this(ConnectionSourcesInitializer.create(connectionSourceFactory, DatastoreUtils.preparePropertyResolver(configuration, "dataSource", "hibernate", "grails")), connectionSourceFactory.getMappingContext(), eventPublisher);
    }

    /**
     * Create a new HibernateDatastore for the given connection sources and mapping context
     *
     * @param configuration The configuration
     * @param connectionSourceFactory The {@link HibernateConnectionSourceFactory} instance
     */
    public HibernateDatastore(PropertyResolver configuration, HibernateConnectionSourceFactory connectionSourceFactory) {
        this(ConnectionSourcesInitializer.create(connectionSourceFactory, DatastoreUtils.preparePropertyResolver(configuration, "dataSource", "hibernate", "grails")), connectionSourceFactory.getMappingContext(), new DefaultApplicationEventPublisher());
    }

    /**
     * Create a new HibernateDatastore for the given connection sources and mapping context
     *
     * @param configuration The configuration
     * @param eventPublisher The {@link ConfigurableApplicationEventPublisher} instance
     * @param classes The persistent classes
     */
    public HibernateDatastore(PropertyResolver configuration, ConfigurableApplicationEventPublisher eventPublisher, Class... classes) {
        this(configuration, new HibernateConnectionSourceFactory(classes), eventPublisher);
    }

    /**
     * Create a new HibernateDatastore for the given connection sources and mapping context
     *
     * @param configuration The configuration
     * @param eventPublisher The {@link ConfigurableApplicationEventPublisher} instance
     * @param classes The persistent classes
     */
    public HibernateDatastore(DataSource dataSource, PropertyResolver configuration, ConfigurableApplicationEventPublisher eventPublisher, Class... classes) {
        this(configuration, createConnectionFactoryForDataSource(dataSource, classes), eventPublisher);
    }

    /**
     * Construct a Hibernate datastore scanning the given packages
     *
     * @param configuration The configuration
     * @param eventPublisher The event publisher
     * @param packagesToScan The packages to scan
     */
    public HibernateDatastore(PropertyResolver configuration, ConfigurableApplicationEventPublisher eventPublisher, Package... packagesToScan) {
        this(configuration, eventPublisher, new ClasspathEntityScanner().scan(packagesToScan));
    }

    /**
     * Construct a Hibernate datastore scanning the given packages for the given datasource
     *
     * @param configuration The configuration
     * @param eventPublisher The event publisher
     * @param packagesToScan The packages to scan
     */
    public HibernateDatastore(DataSource dataSource, PropertyResolver configuration, ConfigurableApplicationEventPublisher eventPublisher, Package... packagesToScan) {
        this(dataSource, configuration, eventPublisher, new ClasspathEntityScanner().scan(packagesToScan));
    }

    /**
     * Create a new HibernateDatastore for the given connection sources and mapping context
     *
     * @param configuration The configuration
     * @param classes The persistent classes
     */
    public HibernateDatastore(PropertyResolver configuration, Class... classes) {
        this(configuration, new HibernateConnectionSourceFactory(classes));
    }

    /**
     * Construct a Hibernate datastore scanning the given packages
     *
     * @param configuration The configuration
     * @param packagesToScan The packages to scan
     */
    public HibernateDatastore(PropertyResolver configuration, Package... packagesToScan) {
        this(configuration, new ClasspathEntityScanner().scan(packagesToScan));
    }

    /**
     * Constructor used purely for testing purposes. Creates a datastore with an in-memory database and dbCreate set to 'create-drop'
     *
     * @param classes The classes
     */
    public HibernateDatastore(Map<String, Object> configuration, Class... classes) {
        this(DatastoreUtils.createPropertyResolver(configuration), new HibernateConnectionSourceFactory(classes));
    }

    /**
     * Construct a Hibernate datastore scanning the given packages
     *
     * @param configuration The configuration
     * @param packagesToScan The packages to scan
     */
    public HibernateDatastore(Map<String, Object> configuration, Package... packagesToScan) {
        this(DatastoreUtils.createPropertyResolver(configuration), packagesToScan);
    }

    /**
     * Constructor used purely for testing purposes. Creates a datastore with an in-memory database and dbCreate set to 'create-drop'
     *
     * @param classes The classes
     */
    public HibernateDatastore(Class... classes) {
        this(DatastoreUtils.createPropertyResolver(Collections.singletonMap(Settings.SETTING_DB_CREATE, "create-drop")), new HibernateConnectionSourceFactory(classes));
    }

    /**
     * Construct a Hibernate datastore scanning the given packages
     *
     * @param packagesToScan The packages to scan
     */
    public HibernateDatastore(Package... packagesToScan) {
        this(new ClasspathEntityScanner().scan(packagesToScan));
    }

    /**
     * Construct a Hibernate datastore scanning the given packages
     *
     * @param packageToScan The package to scan
     */
    public HibernateDatastore(Package packageToScan) {
        this(new ClasspathEntityScanner().scan(packageToScan));
    }

    @Override
    public ApplicationEventPublisher getApplicationEventPublisher() {
        return this.eventPublisher;
    }

    /**
     * @return The {@link org.springframework.transaction.PlatformTransactionManager} instance
     */
    public GrailsHibernateTransactionManager getTransactionManager() {
        return transactionManager;
    }

    /**
     * Obtain a child {@link HibernateDatastore} by connection name
     *
     * @param connectionName The connection name
     *
     * @return The {@link HibernateDatastore}
     */
    public HibernateDatastore getDatastoreForConnection(String connectionName) {
        if (connectionName.equals(Settings.SETTING_DATASOURCE) || connectionName.equals(ConnectionSource.DEFAULT)) {
            return this;
        } else {
            HibernateDatastore hibernateDatastore = this.datastoresByConnectionSource.get(connectionName);
            if (hibernateDatastore == null) {
                throw new ConfigurationException("DataSource not found for name [" + connectionName + "] in configuration. Please check your multiple data sources configuration and try again.");
            }
            return hibernateDatastore;
        }
    }

    @Override
    public String toString() {
        return "HibernateDatastore: " + getDataSourceName();
    }

    @Override
    public HibernateMappingContext getMappingContext() {
        return (HibernateMappingContext) super.getMappingContext();
    }

    @Override
    public void setMessageSource(MessageSource messageSource) {
        HibernateMappingContext mappingContext = getMappingContext();
        ValidatorRegistry validatorRegistry = createValidatorRegistry(messageSource);
        HibernateConnectionSourceSettings settings = getConnectionSources().getDefaultConnectionSource().getSettings();
        configureValidatorRegistry(settings, mappingContext, validatorRegistry, messageSource);
    }

    protected void registerEventListeners(ConfigurableApplicationEventPublisher eventPublisher) {
        eventPublisher.addApplicationListener(autoTimestampEventListener);
        if (multiTenantMode == MultiTenancySettings.MultiTenancyMode.DISCRIMINATOR) {
            eventPublisher.addApplicationListener(new MultiTenantEventListener());
        }
        eventPublisher.addApplicationListener(eventTriggeringInterceptor);
    }

    protected void configureValidatorRegistry(HibernateConnectionSourceSettings settings, HibernateMappingContext mappingContext) {
        StaticMessageSource messageSource = new StaticMessageSource();
        ValidatorRegistry defaultValidatorRegistry = createValidatorRegistry(messageSource);
        configureValidatorRegistry(settings, mappingContext, defaultValidatorRegistry, messageSource);
    }

    protected void configureValidatorRegistry(HibernateConnectionSourceSettings settings, HibernateMappingContext mappingContext, ValidatorRegistry validatorRegistry, MessageSource messageSource) {
        if (validatorRegistry instanceof ConstraintRegistry) {
            ((ConstraintRegistry) validatorRegistry).addConstraintFactory(
                    new MappingContextAwareConstraintFactory(UniqueConstraint.class, messageSource, mappingContext)
            );
        }
        mappingContext.setValidatorRegistry(
                validatorRegistry
        );
    }

    protected HibernateGormEnhancer initialize() {
        final HibernateConnectionSource defaultConnectionSource = (HibernateConnectionSource) getConnectionSources().getDefaultConnectionSource();
        if (multiTenantMode == MultiTenancySettings.MultiTenancyMode.SCHEMA) {
            return new HibernateGormEnhancer(this, transactionManager, defaultConnectionSource.getSettings()) {
                @Override
                public List<String> allQualifiers(Datastore datastore, PersistentEntity entity) {
                    List<String> allQualifiers = super.allQualifiers(datastore, entity);
                    if (MultiTenant.class.isAssignableFrom(entity.getJavaClass())) {
                        if (tenantResolver instanceof AllTenantsResolver) {
                            Iterable<Serializable> tenantIds = ((AllTenantsResolver) tenantResolver).resolveTenantIds();
                            for (Serializable id : tenantIds) {
                                allQualifiers.add(id.toString());
                            }
                        }
                        else {
                            Collection<String> schemaNames = schemaHandler.resolveSchemaNames(defaultConnectionSource.getDataSource());
                            for (String schemaName : schemaNames) {
                                // skip common internal schemas
                                if (schemaName.equals("INFORMATION_SCHEMA") || schemaName.equals("PUBLIC")) continue;
                                for (String connectionName : datastoresByConnectionSource.keySet()) {
                                    if (schemaName.equalsIgnoreCase(connectionName)) {
                                        allQualifiers.add(connectionName);
                                    }
                                }
                            }
                        }
                    }

                    return allQualifiers;
                }
            };
        }
        else {
            return new HibernateGormEnhancer(this, transactionManager, defaultConnectionSource.getSettings());
        }
    }

    @Override
    public boolean hasCurrentSession() {
        return TransactionSynchronizationManager.getResource(sessionFactory) != null;
    }

    @Override
    protected Session createSession(PropertyResolver connectionDetails) {
        return new HibernateSession(this, sessionFactory);
    }

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if (applicationContext instanceof ConfigurableApplicationContext) {
            super.setApplicationContext(applicationContext);

            for (HibernateDatastore hibernateDatastore : datastoresByConnectionSource.values()) {
                if (hibernateDatastore != this) {
                    hibernateDatastore.setApplicationContext(applicationContext);
                }
            }
            this.eventPublisher = new ConfigurableApplicationContextEventPublisher((ConfigurableApplicationContext) applicationContext);
            HibernateConnectionSourceSettings settings = getConnectionSources().getDefaultConnectionSource().getSettings();
            HibernateConnectionSourceSettings.HibernateSettings hibernateSettings = settings.getHibernate();
            ClosureEventTriggeringInterceptor interceptor = (ClosureEventTriggeringInterceptor) hibernateSettings.getEventTriggeringInterceptor();
            interceptor.setDatastore(this);
            interceptor.setEventPublisher(eventPublisher);
            MappingContext mappingContext = getMappingContext();
            // make messages from the application context available to validation
            ValidatorRegistry validatorRegistry = createValidatorRegistry(applicationContext);
            configureValidatorRegistry(settings, (HibernateMappingContext) mappingContext, validatorRegistry, applicationContext);
            mappingContext.setValidatorRegistry(
                    validatorRegistry
            );

            registerEventListeners(eventPublisher);
            this.eventPublisher.publishEvent(new DatastoreInitializedEvent(this));
        }
    }

    @Override
    public IHibernateTemplate getHibernateTemplate(int flushMode) {
        return new GrailsHibernateTemplate(getSessionFactory(), this, flushMode);
    }

    @Override
    public void withFlushMode(FlushMode flushMode, Callable<Boolean> callable) {
        final org.hibernate.Session session = sessionFactory.getCurrentSession();
        org.hibernate.FlushMode previousMode = null;
        Boolean reset = true;
        try {
            if (session != null) {
                previousMode = session.getHibernateFlushMode();
                session.setHibernateFlushMode(org.hibernate.FlushMode.valueOf(flushMode.name()));
            }
            try {
                reset = callable.call();
            } catch (Exception e) {
                reset = false;
            }
        }
        finally {
            if (session != null && previousMode != null && reset) {
                session.setHibernateFlushMode(previousMode);
            }
        }
    }

    @Override
    public org.hibernate.Session openSession() {
        org.hibernate.Session session = this.sessionFactory.openSession();
        session.setHibernateFlushMode(org.hibernate.FlushMode.valueOf(defaultFlushModeName));
        return session;
    }

    @Override
    public Session getCurrentSession() throws ConnectionNotFoundException {
        // HibernateSession, just a thin wrapper around default session handling so simply return a new instance here
        return new HibernateSession(this, sessionFactory, getDefaultFlushMode());
    }

    @Override
    public void destroy() {
        try {
            super.destroy();
        } finally {
            GrailsDomainBinder.clearMappingCache();
            try {
                this.gormEnhancer.close();
            } catch (IOException e) {
                LOG.error("There was an error shutting down GORM enhancer", e);
            }
        }
    }

    @Override
    public void addTenantForSchema(String schemaName) {
        addTenantForSchemaInternal(schemaName);
        registerAllEntitiesWithEnhancer();
        HibernateConnectionSource defaultConnectionSource = (HibernateConnectionSource) connectionSources.getDefaultConnectionSource();
        DataSource dataSource = defaultConnectionSource.getDataSource();
        if (dataSource instanceof TransactionAwareDataSourceProxy) {
            dataSource = ((TransactionAwareDataSourceProxy) dataSource).getTargetDataSource();
        }
        Object existing = TransactionSynchronizationManager.getResource(dataSource);
        if (existing instanceof ConnectionHolder) {
            ConnectionHolder connectionHolder = (ConnectionHolder) existing;
            Connection connection = connectionHolder.getConnection();
            try {
                if (!connection.isClosed() && !connection.isReadOnly()) {
                    schemaHandler.useDefaultSchema(connection);
                }
            } catch (SQLException e) {
                throw new DatastoreConfigurationException("Failed to reset to default schema: " + e.getMessage(), e);
            }
        }

    }

    public Metadata getMetadata() {
        return metadata;
    }

    protected void registerAllEntitiesWithEnhancer() {
        for (PersistentEntity persistentEntity : mappingContext.getPersistentEntities()) {
            gormEnhancer.registerEntity(persistentEntity);
        }
    }

    private void addTenantForSchemaInternal(final String schemaName) {
        if (multiTenantMode != MultiTenancySettings.MultiTenancyMode.SCHEMA) {
            throw new ConfigurationException("The method [addTenantForSchema] can only be called with multi-tenancy mode SCHEMA. Current mode is: " + multiTenantMode);
        }
        HibernateConnectionSourceFactory factory = (HibernateConnectionSourceFactory) connectionSources.getFactory();
        HibernateConnectionSource defaultConnectionSource = (HibernateConnectionSource) connectionSources.getDefaultConnectionSource();
        HibernateConnectionSourceSettings tenantSettings;
        try {
            tenantSettings = (HibernateConnectionSourceSettings) connectionSources.getDefaultConnectionSource().getSettings().clone();
        } catch (CloneNotSupportedException e) {
            throw new ConfigurationException("Couldn't clone default Hibernate settings! " + e.getMessage(), e);
        }
        tenantSettings.getHibernate().put(Environment.DEFAULT_SCHEMA, schemaName);

        String dbCreate = tenantSettings.getDataSource().getDbCreate();

        SchemaAutoTooling schemaAutoTooling = dbCreate != null ? SchemaAutoTooling.interpret(dbCreate) : null;
        if (schemaAutoTooling != null && schemaAutoTooling != SchemaAutoTooling.VALIDATE && schemaAutoTooling != SchemaAutoTooling.NONE) {

            Connection connection = null;
            try {
                connection = defaultConnectionSource.getDataSource().getConnection();
                try {
                    schemaHandler.useSchema(connection, schemaName);
                } catch (Exception e) {
                    // schema doesn't exist
                    schemaHandler.createSchema(connection, schemaName);
                }

            } catch (SQLException e) {
                throw new DatastoreConfigurationException(String.format("Failed to create schema for name [%s]", schemaName));
            }
            finally {
                if (connection != null) {
                    try {
                        schemaHandler.useDefaultSchema(connection);
                        connection.close();
                    } catch (SQLException e) {
                        //ignore
                    }
                }
            }
        }

        DataSource dataSource = defaultConnectionSource.getDataSource();
        dataSource = new MultiTenantDataSource(dataSource, schemaName) {
            @Override
            public Connection getConnection() throws SQLException {
                Connection connection = super.getConnection();
                schemaHandler.useSchema(connection, schemaName);
                return new MultiTenantConnection(connection, schemaHandler);
            }

            @Override
            public Connection getConnection(String username, String password) throws SQLException {
                Connection connection = super.getConnection(username, password);
                schemaHandler.useSchema(connection, schemaName);
                return new MultiTenantConnection(connection, schemaHandler);
            }
        };
        DefaultConnectionSource<DataSource, DataSourceSettings> dataSourceConnectionSource = new DefaultConnectionSource<>(schemaName, dataSource, tenantSettings.getDataSource());
        ConnectionSource<SessionFactory, HibernateConnectionSourceSettings> connectionSource = factory.create(schemaName, dataSourceConnectionSource, tenantSettings);
        SingletonConnectionSources<SessionFactory, HibernateConnectionSourceSettings> singletonConnectionSources = new SingletonConnectionSources<>(connectionSource, connectionSources.getBaseConfiguration());
        HibernateDatastore childDatastore = new HibernateDatastore(singletonConnectionSources, (HibernateMappingContext) mappingContext, eventPublisher) {
            @Override
            protected HibernateGormEnhancer initialize() {
                return null;
            }
        };
        datastoresByConnectionSource.put(connectionSource.getName(), childDatastore);
    }

    private Metadata getMetadataInternal() {
        Metadata metadata = null;
        ServiceRegistry bootstrapServiceRegistry = ((SessionFactoryImplementor) sessionFactory).getServiceRegistry().getParentServiceRegistry();
        Iterable<Integrator> integrators = bootstrapServiceRegistry.getService(IntegratorService.class).getIntegrators();
        for (Integrator integrator : integrators) {
            if (integrator instanceof MetadataIntegrator) {
                metadata = ((MetadataIntegrator) integrator).getMetadata();
            }
        }
        return metadata;
    }

    private static HibernateConnectionSourceFactory createConnectionFactoryForDataSource(final DataSource dataSource, Class... classes) {
        HibernateConnectionSourceFactory hibernateConnectionSourceFactory = new HibernateConnectionSourceFactory(classes);
        hibernateConnectionSourceFactory.setDataSourceConnectionSourceFactory(
                new DataSourceConnectionSourceFactory() {
                    @Override
                    public ConnectionSource<DataSource, DataSourceSettings> create(String name, DataSourceSettings settings) {
                        if (ConnectionSource.DEFAULT.equals(name)) {
                            return new DataSourceConnectionSource(ConnectionSource.DEFAULT, dataSource, settings);
                        }
                        else {
                            return super.create(name, settings);
                        }
                    }
                }
        );
        return hibernateConnectionSourceFactory;
    }
}
