jdaugherty commented on code in PR #15568:
URL: https://github.com/apache/grails-core/pull/15568#discussion_r3196289274


##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/HibernateMappingContextConfiguration.java:
##########
@@ -0,0 +1,421 @@
+/*
+ *  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.cfg;
+
+import java.io.IOException;
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+
+import javax.sql.DataSource;
+
+import jakarta.annotation.Nullable;
+import jakarta.persistence.Embeddable;
+import jakarta.persistence.Entity;
+import jakarta.persistence.MappedSuperclass;
+
+import org.hibernate.HibernateException;
+import org.hibernate.MappingException;
+import org.hibernate.SessionFactory;
+import org.hibernate.boot.registry.BootstrapServiceRegistry;
+import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder;
+import org.hibernate.boot.registry.StandardServiceRegistry;
+import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
+import 
org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl;
+import org.hibernate.boot.registry.classloading.spi.ClassLoaderService;
+import org.hibernate.boot.spi.AdditionalMappingContributor;
+import org.hibernate.cfg.AvailableSettings;
+import org.hibernate.cfg.BytecodeSettings;
+import org.hibernate.cfg.Configuration;
+import org.hibernate.cfg.Environment;
+import org.hibernate.cfg.JdbcSettings;
+import org.hibernate.context.spi.CurrentSessionContext;
+import org.hibernate.internal.util.config.ConfigurationHelper;
+import org.hibernate.service.ServiceRegistry;
+
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternUtils;
+import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
+import org.springframework.core.type.classreading.MetadataReader;
+import org.springframework.core.type.classreading.MetadataReaderFactory;
+import org.springframework.core.type.filter.AnnotationTypeFilter;
+import org.springframework.core.type.filter.TypeFilter;
+import org.springframework.util.ClassUtils;
+
+import org.grails.datastore.gorm.GormEntity;
+import org.grails.datastore.gorm.jdbc.connections.DataSourceSettings;
+import org.grails.datastore.mapping.core.connections.ConnectionSource;
+import org.grails.datastore.mapping.model.PersistentEntity;
+import org.grails.orm.hibernate.EventListenerIntegrator;
+import org.grails.orm.hibernate.GrailsSessionContext;
+import org.grails.orm.hibernate.HibernateEventListeners;
+import org.grails.orm.hibernate.MetadataIntegrator;
+import org.grails.orm.hibernate.cfg.domainbinding.binder.GrailsDomainBinder;
+import org.grails.orm.hibernate.cfg.domainbinding.util.NamingStrategyProvider;
+import org.grails.orm.hibernate.proxy.GrailsBytecodeProvider;
+
+/**
+ * A Configuration that uses a MappingContext to configure Hibernate
+ *
+ * @since 5.0
+ */
+@SuppressWarnings({"rawtypes", "PMD.UseProperClassLoader", 
"PMD.DataflowAnomalyAnalysis", "PMD.CloseResource"})
+public class HibernateMappingContextConfiguration extends Configuration
+        implements ApplicationContextAware, Serializable {
+
+    @Serial
+    private static final long serialVersionUID = -7115087342689305517L;
+
+    private static final String RESOURCE_PATTERN = "/**/*.class";
+
+    private static final TypeFilter[] ENTITY_TYPE_FILTERS = new TypeFilter[] {
+        new AnnotationTypeFilter(Entity.class, false),
+        new AnnotationTypeFilter(Embeddable.class, false),
+        new AnnotationTypeFilter(MappedSuperclass.class, false)
+    };
+    private static final String FALSE_LITERAL = "false";
+    private final Class<? extends CurrentSessionContext> currentSessionContext 
= GrailsSessionContext.class;
+    //    private MetadataContributor metadataContributor;
+    private final Set<Class> additionalClasses = new HashSet<>();
+    protected String sessionFactoryBeanName = "sessionFactory";
+    protected String dataSourceName = ConnectionSource.DEFAULT;
+    protected transient HibernateMappingContext hibernateMappingContext;
+    private transient HibernateEventListeners hibernateEventListeners;
+    private Map<String, Object> eventListeners;
+    private transient ServiceRegistry serviceRegistry;
+    private transient ResourcePatternResolver resourcePatternResolver = new 
PathMatchingResourcePatternResolver();
+    private transient NamingStrategyProvider namingStrategyProvider = new 
NamingStrategyProvider();
+    protected GrailsBytecodeProvider bytecodeProvider;
+
+    public void setBytecodeProvider(GrailsBytecodeProvider bytecodeProvider) {
+        this.bytecodeProvider = bytecodeProvider;
+    }
+
+    public NamingStrategyProvider getNamingStrategyProvider() {
+        return namingStrategyProvider;
+    }
+
+    public void setNamingStrategyProvider(NamingStrategyProvider 
namingStrategyProvider) {
+        this.namingStrategyProvider = namingStrategyProvider;
+    }
+
+    public MappingCacheHolder getMappingCacheHolder() {
+        return hibernateMappingContext != null ? 
hibernateMappingContext.getMappingCacheHolder() : null;
+    }
+
+    public void setHibernateMappingContext(HibernateMappingContext 
hibernateMappingContext) {
+        this.hibernateMappingContext = hibernateMappingContext;
+    }
+
+    @Override
+    public void setApplicationContext(@Nullable ApplicationContext 
applicationContext) throws BeansException {
+        resourcePatternResolver = 
ResourcePatternUtils.getResourcePatternResolver(applicationContext);
+        String dsName = ConnectionSource.DEFAULT.equals(dataSourceName) ? 
"dataSource" : "dataSource_" + dataSourceName;
+        Properties properties = getProperties();
+
+        if (applicationContext != null) {
+            if 
(!properties.containsKey(JdbcSettings.JAKARTA_NON_JTA_DATASOURCE) && 
applicationContext.containsBean(dsName)) {
+                properties.put(JdbcSettings.JAKARTA_NON_JTA_DATASOURCE, 
applicationContext.getBean(dsName));
+            }
+            properties.put(Environment.CURRENT_SESSION_CONTEXT_CLASS, 
currentSessionContext.getName());
+            properties.put(
+                    "hibernate.enhancer.bytecodeprovider.instance",
+                getGrailsBytecodeProvider());
+            properties.put("hibernate.bytecode.allow_enhancement_as_proxy", 
FALSE_LITERAL);
+            properties.put("hibernate.bytecode.enhancement_metadata_cache", 
FALSE_LITERAL);
+            properties.put("hibernate.enhancer.enableLazyInitialization", 
FALSE_LITERAL);
+            properties.put("hibernate.enhancer.enableDirtyTracking", 
FALSE_LITERAL);
+            properties.put("hibernate.enhancer.enableAssociationManagement", 
FALSE_LITERAL);
+            ClassLoader classLoader = applicationContext.getClassLoader();
+            if (classLoader != null) {
+                properties.put(AvailableSettings.CLASSLOADERS, classLoader);
+            }
+        }
+    }
+
+    protected GrailsBytecodeProvider getGrailsBytecodeProvider() {
+        return bytecodeProvider != null ? bytecodeProvider : new 
GrailsBytecodeProvider();
+    }
+
+    /**
+     * Set the target SQL {@link DataSource}
+     *
+     * @param connectionSource The data source to use
+     */
+    public void setDataSourceConnectionSource(ConnectionSource<DataSource, 
DataSourceSettings> connectionSource) {
+        this.dataSourceName = connectionSource.getName();
+        DataSource source = connectionSource.getSource();
+        getProperties().put(JdbcSettings.JAKARTA_NON_JTA_DATASOURCE, source);
+        getProperties().put(Environment.CURRENT_SESSION_CONTEXT_CLASS, 
GrailsSessionContext.class.getName());
+        setBytecodeProvider(getGrailsBytecodeProvider());
+        final ClassLoader contextClassLoader = 
Thread.currentThread().getContextClassLoader();
+        if (contextClassLoader != null &&
+                
contextClassLoader.getClass().getSimpleName().equalsIgnoreCase("RestartClassLoader"))
 {
+            getProperties().put(AvailableSettings.CLASSLOADERS, 
contextClassLoader);
+        } else {
+            getProperties()
+                    .put(
+                            AvailableSettings.CLASSLOADERS,
+                            connectionSource.getClass().getClassLoader());
+        }
+    }
+
+    /**
+     * Add the given annotated classes in a batch.
+     *
+     * @return Configuration
+     * @see #addAnnotatedClass
+     * @see #scanPackages
+     */
+    @Override
+    public Configuration addAnnotatedClasses(Class... annotatedClasses) {
+        for (Class<?> annotatedClass : annotatedClasses) {
+            addAnnotatedClass(annotatedClass);
+        }
+        return this;
+    }
+
+    @Override
+    public Configuration addAnnotatedClass(Class annotatedClass) {
+        additionalClasses.add(annotatedClass);
+        return super.addAnnotatedClass(annotatedClass);
+    }
+
+    @Override
+    public HibernateMappingContextConfiguration addPackages(String... 
annotatedPackages) {
+        for (String annotatedPackage : annotatedPackages) {
+            addPackage(annotatedPackage);
+        }
+        return this;
+    }
+
+    /**
+     * Perform Spring-based scanning for entity classes, registering them as 
annotated classes with
+     * this {@code Configuration}.
+     *
+     * @param packagesToScan one or more Java package names
+     * @throws HibernateException if scanning fails for any reason
+     */
+    public void scanPackages(String... packagesToScan) throws 
HibernateException {
+        try {
+            MetadataReaderFactory readerFactory = new 
CachingMetadataReaderFactory(resourcePatternResolver);
+            for (String pkg : packagesToScan) {
+                String pattern = 
ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
+                        ClassUtils.convertClassNameToResourcePath(pkg) +
+                        RESOURCE_PATTERN;
+                Resource[] resources = 
resourcePatternResolver.getResources(pattern);
+                for (Resource resource : resources) {
+                    if (resource.isReadable()) {
+                        MetadataReader reader = 
readerFactory.getMetadataReader(resource);
+                        String className = 
reader.getClassMetadata().getClassName();
+                        if (matchesFilter(reader, readerFactory)) {
+                            ClassLoader classLoader = 
resourcePatternResolver.getClassLoader();
+                            Class<?> loadedClass = classLoader != null ?
+                                    classLoader.loadClass(className) :
+                                    ClassUtils.forName(className, null);
+                            addAnnotatedClasses(loadedClass);
+                        }
+                    }
+                }
+            }
+        } catch (IOException ex) {
+            throw new MappingException("Failed to scan classpath for unlisted 
classes", ex);
+        } catch (ClassNotFoundException ex) {
+            throw new MappingException("Failed to load annotated classes from 
classpath", ex);
+        }
+    }
+
+    /**
+     * Check whether any of the configured entity type filters matches the 
current class descriptor
+     * contained in the metadata reader.
+     */
+    protected boolean matchesFilter(MetadataReader reader, 
MetadataReaderFactory readerFactory) throws IOException {
+        for (TypeFilter filter : ENTITY_TYPE_FILTERS) {
+            if (filter.match(reader, readerFactory)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public void setSessionFactoryBeanName(String name) {
+        sessionFactoryBeanName = name;
+    }
+
+    public void setDataSourceName(String name) {
+        dataSourceName = name;
+    }
+
+    /* (non-Javadoc)
+     * @see org.hibernate.cfg.Configuration#buildSessionFactory()
+     */
+    @Override
+    public SessionFactory buildSessionFactory() throws HibernateException {
+        // 1. FORCE the custom bytecode provider instance right before 
bootstrap
+        // This bypasses the ServiceLoader and ensures your 
GrailsBytecodeProvider is used.
+        GrailsBytecodeProvider bytecodeProvider = getGrailsBytecodeProvider();
+        getProperties()
+                .put(
+                        BytecodeSettings.BYTECODE_PROVIDER_INSTANCE,
+                        bytecodeProvider);
+
+        // set the class loader to load Groovy classes
+
+        // work around for HHH-2624
+        SessionFactory sessionFactory;
+
+        Object classLoaderObject = 
getProperties().get(AvailableSettings.CLASSLOADERS);
+        ClassLoader appClassLoader;
+
+        if (classLoaderObject instanceof ClassLoader) {
+            appClassLoader = (ClassLoader) classLoaderObject;
+        } else {
+            appClassLoader = getClass().getClassLoader();
+        }
+
+        ConfigurationHelper.resolvePlaceHolders(getProperties());
+
+        final GrailsDomainBinder domainBinder = new GrailsDomainBinder(
+                dataSourceName,
+                sessionFactoryBeanName,
+                hibernateMappingContext,
+                namingStrategyProvider,
+                hibernateMappingContext.getMappingCacheHolder());
+
+        List<Class> annotatedClasses = new ArrayList<>();
+        for (PersistentEntity persistentEntity : 
hibernateMappingContext.getPersistentEntities()) {
+            Class<?> javaClass = persistentEntity.getJavaClass();
+            if (javaClass.isAnnotationPresent(Entity.class)) {
+                annotatedClasses.add(javaClass);
+            }
+        }
+
+        if (!additionalClasses.isEmpty()) {
+            for (Class additionalClass : additionalClasses) {
+                if (GormEntity.class.isAssignableFrom(additionalClass)) {
+                    
hibernateMappingContext.addPersistentEntity(additionalClass);
+                }
+            }
+        }
+
+        addAnnotatedClasses(annotatedClasses.toArray(new Class[0]));
+
+        ClassLoaderService classLoaderService = new 
ClassLoaderServiceImpl(appClassLoader) {
+            @Override
+            public <S> Collection<S> loadJavaServices(Class<S> 
serviceContract) {
+                // Ensure Grails contributes mappings for GORM entities even 
if they lack JPA @Entity
+                if 
(AdditionalMappingContributor.class.isAssignableFrom(serviceContract)) {
+                    // Include the GrailsDomainBinder first, then any other 
contributors
+                    // discovered by the parent classloader (e.g., Envers 
AdditionalMappingContributorImpl).
+                    // Without this, Envers' AdditionalMappingContributor 
would be excluded,
+                    // preventing EnversService from being initialized before 
EnversIntegrator runs.
+                    Collection<S> parentContributors = 
super.loadJavaServices(serviceContract);
+                    @SuppressWarnings("unchecked")
+                    S grailsBinder = (S) domainBinder;
+                    List<S> allContributors = new 
ArrayList<>(parentContributors.size() + 1);
+                    allContributors.add(grailsBinder);
+                    allContributors.addAll(parentContributors);
+                    return allContributors;

Review Comment:
   Good fix. Previously `Collections.singletonList(domainBinder)` silently 
excluded all other `AdditionalMappingContributor` implementations (including 
Envers' `AdditionalMappingContributorImpl`), causing `EnversService` to be 
uninitialized before `EnversIntegrator` ran. Prepending the Grails binder 
before the parent contributors is the correct ordering.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to