jdaugherty commented on code in PR #15409: URL: https://github.com/apache/grails-core/pull/15409#discussion_r2869824545
########## 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: See loadCorePluginsFromResources in the CorePluginFinder; this is how that code has always worked. This isn't threaded and this is how it's worked historically, so I'm not inclined to change it. -- 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]
