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


##########
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:
   I'd argue at this point they shoudln't be using Holders either.  These 
dependencies would have to be autowired and that would happen after the 
constructor creation, so  i view this as low risk.



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