jamesfredley commented on code in PR #15409:
URL: https://github.com/apache/grails-core/pull/15409#discussion_r2835009901


##########
grails-core/src/main/groovy/org/grails/config/GrailsPluginEnvironmentPostProcessor.java:
##########
@@ -0,0 +1,641 @@
+/*
+ *  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.config;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import org.springframework.beans.BeanWrapper;
+import org.springframework.beans.BeanWrapperImpl;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.env.EnvironmentPostProcessor;
+import org.springframework.core.Ordered;
+import org.springframework.core.env.ConfigurableEnvironment;
+import org.springframework.core.env.EnumerablePropertySource;
+import org.springframework.core.env.MutablePropertySources;
+import org.springframework.core.env.PropertySource;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.UrlResource;
+
+import org.grails.config.yaml.YamlPropertySourceLoader;
+import org.grails.core.cfg.GroovyConfigPropertySourceLoader;
+import org.grails.io.support.SpringIOUtils;
+
+/**
+ * A Spring Boot {@link EnvironmentPostProcessor} that loads {@code 
plugin.yml} and
+ * {@code plugin.groovy} configuration files from Grails plugins early in the 
application
+ * lifecycle, before autoconfiguration conditions (such as {@code 
@ConditionalOnProperty})
+ * are evaluated.
+ *
+ * <p>This solves the problem where plugin configuration files were previously 
loaded too
+ * late (during {@code BeanDefinitionRegistryPostProcessor} execution) for 
their properties
+ * to be available to Spring Boot's {@code @ConditionalOnProperty} evaluation 
on
+ * {@code @Configuration} and {@code @AutoConfiguration} classes.</p>
+ *
+ * <h3>Supported Configuration Formats</h3>
+ * <p>Plugins may define configuration in either {@code plugin.yml} (YAML 
format) or
+ * {@code plugin.groovy} (Groovy ConfigSlurper format), but not both. This 
mirrors the
+ * approach originally used by {@code AbstractGrailsPlugin}.</p>
+ *
+ * <h3>Plugin Ordering</h3>
+ * <p>Plugin ordering is respected by:</p>
+ * <ol>
+ *   <li>Discovering plugin classes from {@code META-INF/grails-plugin.xml} 
descriptors</li>
+ *   <li>Instantiating each plugin class just enough to read its {@code 
loadAfter},
+ *       {@code loadBefore}, and {@code dependsOn} ordering metadata</li>
+ *   <li>Performing a topological sort identical to
+ *       {@link grails.plugins.DefaultGrailsPluginManager#sortPlugins}</li>
+ *   <li>Loading configuration files in the sorted order with {@code addLast}
+ *       semantics, so earlier plugins' properties have higher precedence</li>
+ * </ol>
+ *
+ * <p>Property sources are added with the same names and types that
+ * {@link org.grails.plugins.AbstractGrailsPlugin} would produce, ensuring 
consistency
+ * with the rest of the Grails plugin configuration system.</p>
+ *
+ * @since 7.0
+ * @see grails.boot.config.GrailsApplicationPostProcessor
+ */
+public class GrailsPluginEnvironmentPostProcessor implements 
EnvironmentPostProcessor, Ordered {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(GrailsPluginEnvironmentPostProcessor.class);
+
+    /**
+     * The classpath location of the Grails plugin descriptor XML files.
+     */
+    private static final String CORE_PLUGIN_PATTERN = 
"META-INF/grails-plugin.xml";
+
+    /**
+     * The filename for YAML-based plugin configuration.
+     */
+    public static final String PLUGIN_YML = "plugin.yml";
+
+    private static final String PLUGIN_YML_PATH = "/" + PLUGIN_YML;
+
+    /**
+     * The filename for Groovy ConfigSlurper-based plugin configuration.
+     */
+    public static final String PLUGIN_GROOVY = "plugin.groovy";
+
+    private static final String PLUGIN_GROOVY_PATH = "/" + PLUGIN_GROOVY;
+    private static final String GRAILS_PLUGIN_SUFFIX = "GrailsPlugin";
+    private static final List<String> DEFAULT_CONFIG_IGNORE_LIST = 
Arrays.asList("dataSource", "hibernate");
+
+    @Override
+    public int getOrder() {
+        // Run after Spring Boot property source loading but before 
autoconfiguration evaluation.
+        // We use a value that ensures we run after standard property sources 
are loaded but before
+        // autoconfiguration conditions are evaluated.
+        return Ordered.HIGHEST_PRECEDENCE + 15;
+    }
+
+    @Override
+    public void postProcessEnvironment(ConfigurableEnvironment environment, 
SpringApplication application) {
+        try {
+            List<PluginInfo> pluginInfos = discoverPlugins();
+            if (pluginInfos.isEmpty()) {
+                LOG.debug("No Grails plugin classes found in 
META-INF/grails-plugin.xml descriptors");
+                return;
+            }
+
+            List<PluginInfo> sorted = sortPlugins(pluginInfos);
+            loadPluginConfigurations(sorted, environment);
+        } catch (Exception e) {
+            LOG.warn("Error loading Grails plugin configurations early: {}. " +
+                    "Plugin configurations may not be available for 
@ConditionalOnProperty evaluation.",
+                    e.getMessage());
+            if (LOG.isDebugEnabled()) {
+                LOG.debug("Full stack trace:", e);
+            }
+        }
+    }
+
+    /**
+     * Discovers all plugin classes by scanning {@code 
META-INF/grails-plugin.xml}
+     * descriptors on the classpath, then reads ordering metadata from each 
plugin class.
+     */
+    List<PluginInfo> discoverPlugins() {
+        List<String> pluginClassNames = scanPluginDescriptors();
+        if (pluginClassNames.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        List<PluginInfo> pluginInfos = new ArrayList<>();
+        ClassLoader classLoader = 
Thread.currentThread().getContextClassLoader();
+
+        for (String className : pluginClassNames) {
+            try {
+                Class<?> pluginClass = classLoader.loadClass(className);
+                PluginInfo info = extractPluginInfo(pluginClass);
+                if (info != null) {
+                    pluginInfos.add(info);
+                }
+            } catch (ClassNotFoundException e) {
+                LOG.debug("Plugin class [{}] not found, skipping", className);
+            } catch (Exception e) {
+                LOG.debug("Error loading plugin class [{}]: {}", className, 
e.getMessage());
+            }
+        }
+
+        return pluginInfos;
+    }
+
+    /**
+     * Scans all {@code META-INF/grails-plugin.xml} resources on the classpath
+     * and extracts plugin class names using SAX parsing (same approach as
+     * {@link org.grails.plugins.CorePluginFinder}).
+     */
+    List<String> scanPluginDescriptors() {
+        List<String> pluginClassNames = new ArrayList<>();
+        ClassLoader classLoader = 
Thread.currentThread().getContextClassLoader();
+
+        try {
+            Enumeration<URL> resources = 
classLoader.getResources(CORE_PLUGIN_PATTERN);
+            SAXParser saxParser = SpringIOUtils.newSAXParser();
+
+            while (resources.hasMoreElements()) {
+                URL url = resources.nextElement();
+                try (InputStream input = url.openStream()) {
+                    PluginXmlHandler handler = new PluginXmlHandler();
+                    saxParser.parse(input, handler);
+                    pluginClassNames.addAll(handler.getPluginTypes());
+                } catch (IOException | SAXException e) {
+                    LOG.debug("Error parsing plugin descriptor at [{}]: {}", 
url, e.getMessage());
+                }
+            }
+        } catch (IOException | ParserConfigurationException | SAXException e) {
+            LOG.debug("Error scanning for plugin descriptors: {}", 
e.getMessage());
+        }
+
+        return pluginClassNames;
+    }
+
+    /**
+     * Extracts plugin ordering metadata and configuration resource location 
from a
+     * plugin class without requiring a full {@code GrailsApplication} or 
plugin manager.
+     *
+     * <p>The plugin class is instantiated to read its {@code loadAfter}, 
{@code loadBefore},
+     * and {@code dependsOn} properties via a {@link BeanWrapper}, mirroring 
the approach
+     * used by {@link org.grails.plugins.DefaultGrailsPlugin}.</p>
+     *
+     * <p>Configuration is discovered by probing for both {@code plugin.yml} 
and
+     * {@code plugin.groovy} relative to the plugin class, replicating the 
approach from
+     * {@link 
org.grails.plugins.AbstractGrailsPlugin#readPluginConfiguration}. If both
+     * exist, a {@link RuntimeException} is thrown.</p>
+     */
+    PluginInfo extractPluginInfo(Class<?> pluginClass) {

Review Comment:
   calls `pluginClass.getDeclaredConstructor().newInstance()` to read 
`loadAfter`/`loadBefore`/`dependsOn`. At this point in the Spring Boot 
lifecycle, there's no `GrailsApplication`, no application context, no plugin 
manager. If any plugin's no-arg constructor has side effects or requires 
dependencies, this could fail.
   
   The code handles this gracefully (catches exceptions, continues with default 
ordering), but plugins with complex constructors will silently get default 
ordering rather than their declared ordering. This could subtly change the 
order configs are loaded compared to the existing behavior.



##########
grails-core/src/main/groovy/org/grails/config/GrailsPluginEnvironmentPostProcessor.java:
##########
@@ -0,0 +1,641 @@
+/*
+ *  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.config;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import org.springframework.beans.BeanWrapper;
+import org.springframework.beans.BeanWrapperImpl;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.env.EnvironmentPostProcessor;
+import org.springframework.core.Ordered;
+import org.springframework.core.env.ConfigurableEnvironment;
+import org.springframework.core.env.EnumerablePropertySource;
+import org.springframework.core.env.MutablePropertySources;
+import org.springframework.core.env.PropertySource;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.UrlResource;
+
+import org.grails.config.yaml.YamlPropertySourceLoader;
+import org.grails.core.cfg.GroovyConfigPropertySourceLoader;
+import org.grails.io.support.SpringIOUtils;
+
+/**
+ * A Spring Boot {@link EnvironmentPostProcessor} that loads {@code 
plugin.yml} and
+ * {@code plugin.groovy} configuration files from Grails plugins early in the 
application
+ * lifecycle, before autoconfiguration conditions (such as {@code 
@ConditionalOnProperty})
+ * are evaluated.
+ *
+ * <p>This solves the problem where plugin configuration files were previously 
loaded too
+ * late (during {@code BeanDefinitionRegistryPostProcessor} execution) for 
their properties
+ * to be available to Spring Boot's {@code @ConditionalOnProperty} evaluation 
on
+ * {@code @Configuration} and {@code @AutoConfiguration} classes.</p>
+ *
+ * <h3>Supported Configuration Formats</h3>
+ * <p>Plugins may define configuration in either {@code plugin.yml} (YAML 
format) or
+ * {@code plugin.groovy} (Groovy ConfigSlurper format), but not both. This 
mirrors the
+ * approach originally used by {@code AbstractGrailsPlugin}.</p>
+ *
+ * <h3>Plugin Ordering</h3>
+ * <p>Plugin ordering is respected by:</p>
+ * <ol>
+ *   <li>Discovering plugin classes from {@code META-INF/grails-plugin.xml} 
descriptors</li>
+ *   <li>Instantiating each plugin class just enough to read its {@code 
loadAfter},
+ *       {@code loadBefore}, and {@code dependsOn} ordering metadata</li>
+ *   <li>Performing a topological sort identical to
+ *       {@link grails.plugins.DefaultGrailsPluginManager#sortPlugins}</li>
+ *   <li>Loading configuration files in the sorted order with {@code addLast}
+ *       semantics, so earlier plugins' properties have higher precedence</li>
+ * </ol>
+ *
+ * <p>Property sources are added with the same names and types that
+ * {@link org.grails.plugins.AbstractGrailsPlugin} would produce, ensuring 
consistency
+ * with the rest of the Grails plugin configuration system.</p>
+ *
+ * @since 7.0
+ * @see grails.boot.config.GrailsApplicationPostProcessor
+ */
+public class GrailsPluginEnvironmentPostProcessor implements 
EnvironmentPostProcessor, Ordered {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(GrailsPluginEnvironmentPostProcessor.class);
+
+    /**
+     * The classpath location of the Grails plugin descriptor XML files.
+     */
+    private static final String CORE_PLUGIN_PATTERN = 
"META-INF/grails-plugin.xml";
+
+    /**
+     * The filename for YAML-based plugin configuration.
+     */
+    public static final String PLUGIN_YML = "plugin.yml";
+
+    private static final String PLUGIN_YML_PATH = "/" + PLUGIN_YML;
+
+    /**
+     * The filename for Groovy ConfigSlurper-based plugin configuration.
+     */
+    public static final String PLUGIN_GROOVY = "plugin.groovy";
+
+    private static final String PLUGIN_GROOVY_PATH = "/" + PLUGIN_GROOVY;
+    private static final String GRAILS_PLUGIN_SUFFIX = "GrailsPlugin";
+    private static final List<String> DEFAULT_CONFIG_IGNORE_LIST = 
Arrays.asList("dataSource", "hibernate");
+
+    @Override
+    public int getOrder() {
+        // Run after Spring Boot property source loading but before 
autoconfiguration evaluation.
+        // We use a value that ensures we run after standard property sources 
are loaded but before
+        // autoconfiguration conditions are evaluated.
+        return Ordered.HIGHEST_PRECEDENCE + 15;
+    }
+
+    @Override
+    public void postProcessEnvironment(ConfigurableEnvironment environment, 
SpringApplication application) {
+        try {
+            List<PluginInfo> pluginInfos = discoverPlugins();
+            if (pluginInfos.isEmpty()) {
+                LOG.debug("No Grails plugin classes found in 
META-INF/grails-plugin.xml descriptors");
+                return;
+            }
+
+            List<PluginInfo> sorted = sortPlugins(pluginInfos);
+            loadPluginConfigurations(sorted, environment);
+        } catch (Exception e) {
+            LOG.warn("Error loading Grails plugin configurations early: {}. " +
+                    "Plugin configurations may not be available for 
@ConditionalOnProperty evaluation.",
+                    e.getMessage());
+            if (LOG.isDebugEnabled()) {
+                LOG.debug("Full stack trace:", e);
+            }
+        }
+    }
+
+    /**
+     * Discovers all plugin classes by scanning {@code 
META-INF/grails-plugin.xml}
+     * descriptors on the classpath, then reads ordering metadata from each 
plugin class.
+     */
+    List<PluginInfo> discoverPlugins() {
+        List<String> pluginClassNames = scanPluginDescriptors();
+        if (pluginClassNames.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        List<PluginInfo> pluginInfos = new ArrayList<>();
+        ClassLoader classLoader = 
Thread.currentThread().getContextClassLoader();
+
+        for (String className : pluginClassNames) {
+            try {
+                Class<?> pluginClass = classLoader.loadClass(className);
+                PluginInfo info = extractPluginInfo(pluginClass);
+                if (info != null) {
+                    pluginInfos.add(info);
+                }
+            } catch (ClassNotFoundException e) {
+                LOG.debug("Plugin class [{}] not found, skipping", className);
+            } catch (Exception e) {
+                LOG.debug("Error loading plugin class [{}]: {}", className, 
e.getMessage());
+            }
+        }
+
+        return pluginInfos;
+    }
+
+    /**
+     * Scans all {@code META-INF/grails-plugin.xml} resources on the classpath
+     * and extracts plugin class names using SAX parsing (same approach as
+     * {@link org.grails.plugins.CorePluginFinder}).
+     */
+    List<String> scanPluginDescriptors() {
+        List<String> pluginClassNames = new ArrayList<>();
+        ClassLoader classLoader = 
Thread.currentThread().getContextClassLoader();
+
+        try {
+            Enumeration<URL> resources = 
classLoader.getResources(CORE_PLUGIN_PATTERN);
+            SAXParser saxParser = SpringIOUtils.newSAXParser();
+
+            while (resources.hasMoreElements()) {
+                URL url = resources.nextElement();
+                try (InputStream input = url.openStream()) {
+                    PluginXmlHandler handler = new PluginXmlHandler();
+                    saxParser.parse(input, handler);
+                    pluginClassNames.addAll(handler.getPluginTypes());
+                } catch (IOException | SAXException e) {
+                    LOG.debug("Error parsing plugin descriptor at [{}]: {}", 
url, e.getMessage());
+                }
+            }
+        } catch (IOException | ParserConfigurationException | SAXException e) {
+            LOG.debug("Error scanning for plugin descriptors: {}", 
e.getMessage());
+        }
+
+        return pluginClassNames;
+    }
+
+    /**
+     * Extracts plugin ordering metadata and configuration resource location 
from a
+     * plugin class without requiring a full {@code GrailsApplication} or 
plugin manager.
+     *
+     * <p>The plugin class is instantiated to read its {@code loadAfter}, 
{@code loadBefore},
+     * and {@code dependsOn} properties via a {@link BeanWrapper}, mirroring 
the approach
+     * used by {@link org.grails.plugins.DefaultGrailsPlugin}.</p>
+     *
+     * <p>Configuration is discovered by probing for both {@code plugin.yml} 
and
+     * {@code plugin.groovy} relative to the plugin class, replicating the 
approach from
+     * {@link 
org.grails.plugins.AbstractGrailsPlugin#readPluginConfiguration}. If both
+     * exist, a {@link RuntimeException} is thrown.</p>
+     */
+    PluginInfo extractPluginInfo(Class<?> pluginClass) {
+        if (pluginClass == null || 
!pluginClass.getName().endsWith(GRAILS_PLUGIN_SUFFIX)) {
+            return null;
+        }
+
+        String pluginName = getLogicalPluginName(pluginClass);
+
+        // Find the plugin configuration resource (plugin.yml or 
plugin.groovy),
+        // replicating the original 
AbstractGrailsPlugin.readPluginConfiguration() approach
+        Resource configResource = readPluginConfiguration(pluginClass);
+
+        // Extract ordering metadata
+        String[] loadAfterNames = {};
+        String[] loadBeforeNames = {};
+        String[] dependsOnNames = {};
+
+        try {
+            Object pluginInstance = 
pluginClass.getDeclaredConstructor().newInstance();
+            BeanWrapper beanWrapper = new BeanWrapperImpl(pluginInstance);
+
+            loadAfterNames = readStringListProperty(beanWrapper, "loadAfter");
+            loadBeforeNames = readStringListProperty(beanWrapper, 
"loadBefore");
+            dependsOnNames = readDependsOnNames(beanWrapper);
+        } catch (Exception e) {
+            LOG.debug("Could not extract ordering metadata from plugin [{}]: 
{}. " +
+                    "Plugin will be loaded with default ordering.", 
pluginName, e.getMessage());
+        }
+
+        return new PluginInfo(pluginName, pluginClass, configResource,
+                loadAfterNames, loadBeforeNames, dependsOnNames);
+    }
+
+    /**
+     * Derives the logical plugin name from the plugin class, following Grails 
conventions.
+     * For example, {@code org.grails.plugins.CoreGrailsPlugin} becomes {@code 
core}.
+     *
+     * <p>This replicates the logic in {@code 
GrailsNameUtils.getLogicalPropertyName}
+     * and {@code AbstractGrailsClass} without requiring those dependencies at 
this
+     * early lifecycle stage.</p>
+     */
+    String getLogicalPluginName(Class<?> pluginClass) {
+        String shortName = pluginClass.getSimpleName();
+        if (shortName.endsWith(GRAILS_PLUGIN_SUFFIX)) {
+            String withoutSuffix = shortName.substring(0, shortName.length() - 
GRAILS_PLUGIN_SUFFIX.length());
+            if (withoutSuffix.isEmpty()) {
+                return shortName.substring(0, 1).toLowerCase();
+            }
+            // Convert to property name (first char lowercase)
+            return Character.toLowerCase(withoutSuffix.charAt(0)) + 
withoutSuffix.substring(1);
+        }
+        return shortName;
+    }
+
+    /**
+     * Reads the plugin configuration resource by probing for both {@code 
plugin.yml} and
+     * {@code plugin.groovy} relative to the plugin class. Returns the 
resource for whichever
+     * exists, or {@code null} if neither exists. Throws if both exist.
+     *
+     * <p>This replicates the approach from
+     * {@link 
org.grails.plugins.AbstractGrailsPlugin#readPluginConfiguration}.</p>
+     *
+     * @param pluginClass the plugin class to resolve relative to
+     * @return the configuration resource, or {@code null} if no config file 
found
+     */
+    Resource readPluginConfiguration(Class<?> pluginClass) {
+        Resource ymlResource = getConfigurationResource(pluginClass, 
PLUGIN_YML_PATH);
+        Resource groovyResource = getConfigurationResource(pluginClass, 
PLUGIN_GROOVY_PATH);
+
+        boolean groovyResourceExists = groovyResource != null && 
groovyResource.exists();
+
+        if (ymlResource != null && ymlResource.exists()) {
+            if (groovyResourceExists) {
+                throw new RuntimeException("A plugin [" + 
pluginClass.getName() +
+                        "] may define a plugin.yml or a plugin.groovy, but not 
both");
+            }
+            return ymlResource;
+        }
+        if (groovyResourceExists) {
+            return groovyResource;
+        }
+        return null;
+    }
+
+    /**
+     * Finds a plugin configuration resource at the given path relative to the 
plugin class,
+     * replicating the logic in {@link 
grails.io.IOUtils#findResourceRelativeToClass}.
+     *
+     * @param pluginClass the plugin class to resolve relative to
+     * @param configPath the path to probe (e.g., {@code "/plugin.yml"} or 
{@code "/plugin.groovy"})
+     * @return the resource wrapping the configuration URL, or {@code null} if 
not found
+     */
+    Resource getConfigurationResource(Class<?> pluginClass, String configPath) 
{

Review Comment:
   reimplements the logic from `IOUtils.findResourceRelativeToClass` instead of 
calling it. The logic is nearly identical, but there's one difference: the 
original uses `BuildSettings.BUILD_CLASSES_PATH` constant for the dev-mode path 
check, while the new code hardcodes `"build/classes/groovy/main"`. If 
`BuildSettings.BUILD_CLASSES_PATH` ever changes, these would diverge. However, 
using `IOUtils` directly would pull in the `grails-gradle-model` dependency 
which may not be desirable at this early lifecycle point.



##########
grails-core/src/main/groovy/org/grails/config/GrailsPluginEnvironmentPostProcessor.java:
##########
@@ -0,0 +1,641 @@
+/*
+ *  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.config;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import org.springframework.beans.BeanWrapper;
+import org.springframework.beans.BeanWrapperImpl;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.env.EnvironmentPostProcessor;
+import org.springframework.core.Ordered;
+import org.springframework.core.env.ConfigurableEnvironment;
+import org.springframework.core.env.EnumerablePropertySource;
+import org.springframework.core.env.MutablePropertySources;
+import org.springframework.core.env.PropertySource;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.UrlResource;
+
+import org.grails.config.yaml.YamlPropertySourceLoader;
+import org.grails.core.cfg.GroovyConfigPropertySourceLoader;
+import org.grails.io.support.SpringIOUtils;
+
+/**
+ * A Spring Boot {@link EnvironmentPostProcessor} that loads {@code 
plugin.yml} and
+ * {@code plugin.groovy} configuration files from Grails plugins early in the 
application
+ * lifecycle, before autoconfiguration conditions (such as {@code 
@ConditionalOnProperty})
+ * are evaluated.
+ *
+ * <p>This solves the problem where plugin configuration files were previously 
loaded too
+ * late (during {@code BeanDefinitionRegistryPostProcessor} execution) for 
their properties
+ * to be available to Spring Boot's {@code @ConditionalOnProperty} evaluation 
on
+ * {@code @Configuration} and {@code @AutoConfiguration} classes.</p>
+ *
+ * <h3>Supported Configuration Formats</h3>
+ * <p>Plugins may define configuration in either {@code plugin.yml} (YAML 
format) or
+ * {@code plugin.groovy} (Groovy ConfigSlurper format), but not both. This 
mirrors the
+ * approach originally used by {@code AbstractGrailsPlugin}.</p>
+ *
+ * <h3>Plugin Ordering</h3>
+ * <p>Plugin ordering is respected by:</p>
+ * <ol>
+ *   <li>Discovering plugin classes from {@code META-INF/grails-plugin.xml} 
descriptors</li>
+ *   <li>Instantiating each plugin class just enough to read its {@code 
loadAfter},
+ *       {@code loadBefore}, and {@code dependsOn} ordering metadata</li>
+ *   <li>Performing a topological sort identical to
+ *       {@link grails.plugins.DefaultGrailsPluginManager#sortPlugins}</li>
+ *   <li>Loading configuration files in the sorted order with {@code addLast}
+ *       semantics, so earlier plugins' properties have higher precedence</li>
+ * </ol>
+ *
+ * <p>Property sources are added with the same names and types that
+ * {@link org.grails.plugins.AbstractGrailsPlugin} would produce, ensuring 
consistency
+ * with the rest of the Grails plugin configuration system.</p>
+ *
+ * @since 7.0
+ * @see grails.boot.config.GrailsApplicationPostProcessor
+ */
+public class GrailsPluginEnvironmentPostProcessor implements 
EnvironmentPostProcessor, Ordered {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(GrailsPluginEnvironmentPostProcessor.class);
+
+    /**
+     * The classpath location of the Grails plugin descriptor XML files.
+     */
+    private static final String CORE_PLUGIN_PATTERN = 
"META-INF/grails-plugin.xml";
+
+    /**
+     * The filename for YAML-based plugin configuration.
+     */
+    public static final String PLUGIN_YML = "plugin.yml";
+
+    private static final String PLUGIN_YML_PATH = "/" + PLUGIN_YML;
+
+    /**
+     * The filename for Groovy ConfigSlurper-based plugin configuration.
+     */
+    public static final String PLUGIN_GROOVY = "plugin.groovy";
+
+    private static final String PLUGIN_GROOVY_PATH = "/" + PLUGIN_GROOVY;
+    private static final String GRAILS_PLUGIN_SUFFIX = "GrailsPlugin";
+    private static final List<String> DEFAULT_CONFIG_IGNORE_LIST = 
Arrays.asList("dataSource", "hibernate");
+
+    @Override
+    public int getOrder() {
+        // Run after Spring Boot property source loading but before 
autoconfiguration evaluation.
+        // We use a value that ensures we run after standard property sources 
are loaded but before
+        // autoconfiguration conditions are evaluated.
+        return Ordered.HIGHEST_PRECEDENCE + 15;
+    }
+
+    @Override
+    public void postProcessEnvironment(ConfigurableEnvironment environment, 
SpringApplication application) {
+        try {
+            List<PluginInfo> pluginInfos = discoverPlugins();
+            if (pluginInfos.isEmpty()) {
+                LOG.debug("No Grails plugin classes found in 
META-INF/grails-plugin.xml descriptors");
+                return;
+            }
+
+            List<PluginInfo> sorted = sortPlugins(pluginInfos);
+            loadPluginConfigurations(sorted, environment);
+        } catch (Exception e) {
+            LOG.warn("Error loading Grails plugin configurations early: {}. " +
+                    "Plugin configurations may not be available for 
@ConditionalOnProperty evaluation.",
+                    e.getMessage());
+            if (LOG.isDebugEnabled()) {
+                LOG.debug("Full stack trace:", e);
+            }
+        }
+    }
+
+    /**
+     * Discovers all plugin classes by scanning {@code 
META-INF/grails-plugin.xml}
+     * descriptors on the classpath, then reads ordering metadata from each 
plugin class.
+     */
+    List<PluginInfo> discoverPlugins() {
+        List<String> pluginClassNames = scanPluginDescriptors();
+        if (pluginClassNames.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        List<PluginInfo> pluginInfos = new ArrayList<>();
+        ClassLoader classLoader = 
Thread.currentThread().getContextClassLoader();
+
+        for (String className : pluginClassNames) {
+            try {
+                Class<?> pluginClass = classLoader.loadClass(className);
+                PluginInfo info = extractPluginInfo(pluginClass);
+                if (info != null) {
+                    pluginInfos.add(info);
+                }
+            } catch (ClassNotFoundException e) {
+                LOG.debug("Plugin class [{}] not found, skipping", className);
+            } catch (Exception e) {
+                LOG.debug("Error loading plugin class [{}]: {}", className, 
e.getMessage());
+            }
+        }
+
+        return pluginInfos;
+    }
+
+    /**
+     * Scans all {@code META-INF/grails-plugin.xml} resources on the classpath
+     * and extracts plugin class names using SAX parsing (same approach as
+     * {@link org.grails.plugins.CorePluginFinder}).
+     */
+    List<String> scanPluginDescriptors() {
+        List<String> pluginClassNames = new ArrayList<>();
+        ClassLoader classLoader = 
Thread.currentThread().getContextClassLoader();
+
+        try {
+            Enumeration<URL> resources = 
classLoader.getResources(CORE_PLUGIN_PATTERN);
+            SAXParser saxParser = SpringIOUtils.newSAXParser();

Review Comment:
    A single `SAXParser` instance is created and reused across all 
`grails-plugin.xml` files. Per the SAX spec, `SAXParser` instances are not 
guaranteed to be reusable after `parse()`. The `javax.xml.parsers.SAXParser` 
javadoc says "An implementation of SAXParser is NOT guaranteed to behave as per 
the specification if it is used concurrently by two or more threads." More 
importantly, some implementations don't support reuse at all. A new parser 
should be created per file, or `reset()` should be called between uses.



-- 
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