package org.jboss.windup.ext.groovy; import groovy.lang.Binding; import groovy.lang.GroovyShell; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.net.URL; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Logger; import javax.inject.Inject; import org.codehaus.groovy.control.CompilerConfiguration; import org.jboss.forge.furnace.Furnace; import org.jboss.forge.furnace.addons.Addon; import org.jboss.forge.furnace.addons.AddonDependency; import org.jboss.forge.furnace.addons.AddonFilter; import org.jboss.forge.furnace.services.Imported; import org.jboss.windup.config.AbstractRuleProvider; import org.jboss.windup.config.RuleProvider; import org.jboss.windup.config.builder.RuleProviderBuilder; import org.jboss.windup.config.loader.RuleLoaderContext; import org.jboss.windup.config.loader.RuleProviderLoader; import org.jboss.windup.util.FurnaceCompositeClassLoader; import org.jboss.windup.util.Logging; import org.jboss.windup.util.exception.WindupException; import org.jboss.windup.util.furnace.FurnaceClasspathScanner; /** * Loads files with the specified extension (specified in {@link GroovyWindupRuleProviderLoader#GROOVY_RULES_EXTENSION} * ), interprets them as Groovy scripts, and returns the resulting {@link AbstractRuleProvider}s. * * @author <a href="mailto:jesse.sightler@gmail.com">Jesse Sightler</a> * */ public class GroovyWindupRuleProviderLoader implements RuleProviderLoader { private static final Logger LOG = Logging.get(GroovyWindupRuleProviderLoader.class); public static final String CURRENT_WINDUP_SCRIPT = "CURRENT_WINDUP_SCRIPT"; private static final String GROOVY_RULES_EXTENSION = "windup.groovy"; @Inject private FurnaceClasspathScanner scanner; @Inject private Furnace furnace; @Inject private Imported<GroovyConfigMethod> methods; @Override public boolean isFileBased() { return true; } @Override @SuppressWarnings("unchecked") public List<RuleProvider> getProviders(final RuleLoaderContext ruleLoaderContext) { final List<RuleProvider> results = new ArrayList<>(); Binding binding = new Binding(); binding.setVariable("supportFunctions", new HashMap<>()); binding.setVariable("ruleLoaderContext", ruleLoaderContext); GroovyConfigContext configContext = () -> ruleLoaderContext; for (GroovyConfigMethod method : methods) { binding.setVariable(method.getName(configContext), method.getClosure(configContext)); } CompilerConfiguration config = new CompilerConfiguration(); // TODO import everything!!! config.addCompilationCustomizers(new ImportCustomizer()); ClassLoader loader = getCompositeClassloader(); GroovyShell shell = new GroovyShell(loader, binding, config); // import all the support functions defined in a separate groovy file try (InputStream supportFuncsIS = getClass().getResourceAsStream( "/org/jboss/windup/addon/groovy/WindupGroovySupportFunctions.groovy")) { InputStreamReader isr = new InputStreamReader(supportFuncsIS); shell.evaluate(isr); } catch (Exception e) { throw new WindupException("Failed to load support functions due to: " + e.getMessage(), e); } Map<String, ?> supportFunctions = (Map<String, ?>) binding.getVariable("supportFunctions"); for (Map.Entry<String, ?> supportFunctionEntry : supportFunctions.entrySet()) { binding.setVariable(supportFunctionEntry.getKey(), supportFunctionEntry.getValue()); } binding.setVariable("supportFunctions", null); String scriptPath = null; for (URL resource : getScripts(ruleLoaderContext)) { try (Reader reader = new InputStreamReader(resource.openStream())) { List<AbstractRuleProvider> ruleProviders = new ArrayList<>(); binding.setVariable("windupRuleProviderBuilders", ruleProviders); scriptPath = resource.toExternalForm(); binding.setVariable(CURRENT_WINDUP_SCRIPT, scriptPath); shell.evaluate(reader); List<AbstractRuleProvider> providers = (List<AbstractRuleProvider>) binding.getVariable("windupRuleProviderBuilders"); for (AbstractRuleProvider provider : providers) { if (provider instanceof RuleProviderBuilder) { ((RuleProviderBuilder) provider).setOrigin(scriptPath); } results.add(provider); } } catch (Exception e) { throw new WindupException("Failed to evaluate configuration from script [" + scriptPath + "]: ", e); } } return results; } private ClassLoader getCompositeClassloader() { List<ClassLoader> loaders = new ArrayList<>(); AddonFilter filter = new AddonFilter() { @Override public boolean accept(Addon addon) { Set<AddonDependency> dependencies = addon.getDependencies(); for (AddonDependency dependency : dependencies) { // TODO this should only accept addons that depend on windup-config-groovy or whatever we call that if (dependency.getDependency().getId().getName().contains("groovy")) { return true; } } return false; } }; for (Addon addon : furnace.getAddonRegistry().getAddons(filter)) { loaders.add(addon.getClassLoader()); } return new FurnaceCompositeClassLoader(getClass().getClassLoader(), loaders); } private Iterable<URL> getScripts(RuleLoaderContext ruleLoaderContext) { List<URL> results = new ArrayList<>(); List<URL> scripts = scanner.scan(GROOVY_RULES_EXTENSION); results.addAll(scripts); for (Path userRulesPath : ruleLoaderContext.getRulePaths()) { results.addAll(getScripts(userRulesPath)); } return results; } private Collection<URL> getScripts(Path userRulesPath) { try { // Deal with the case of a single file here if (Files.isRegularFile(userRulesPath) && pathMatchesNamePattern(userRulesPath)) return Collections.singletonList(userRulesPath.toUri().toURL()); final List<URL> results = new ArrayList<>(); if (!Files.isDirectory(userRulesPath)) { LOG.warning("Not scanning: " + userRulesPath.normalize().toString() + " for rules as the directory could not be found!"); return Collections.emptyList(); } Files.walkFileTree(userRulesPath, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { if (pathMatchesNamePattern(file)) { results.add(file.toUri().toURL()); } return FileVisitResult.CONTINUE; } }); return results; } catch (IOException e) { throw new WindupException("Failed to search userdir: \"" + userRulesPath + "\" for groovy rules due to: " + e.getMessage(), e); } } private boolean pathMatchesNamePattern(Path file) { return file.getFileName().toString().toLowerCase().endsWith("." + GROOVY_RULES_EXTENSION); } }