/*
* Copyright (c) MuleSoft, Inc. All rights reserved. http://www.mulesoft.com
* The software in this package is published under the terms of the CPAL v1.0
* license, a copy of which has been included with this distribution in the
* LICENSE.txt file.
*/
package org.mule.test.runner.api;
import static com.google.common.collect.Lists.newArrayList;
import static java.io.File.separator;
import static org.mule.test.runner.api.MulePluginBasedLoaderFinder.META_INF_MULE_PLUGIN;
import org.mule.runtime.api.lifecycle.InitialisationException;
import org.mule.runtime.api.meta.model.ExtensionModel;
import org.mule.runtime.core.DefaultMuleContext;
import org.mule.runtime.core.api.extension.ExtensionManager;
import org.mule.runtime.core.api.registry.MuleRegistry;
import org.mule.runtime.core.exception.ErrorTypeLocator;
import org.mule.runtime.core.exception.ErrorTypeLocatorFactory;
import org.mule.runtime.core.exception.ErrorTypeRepository;
import org.mule.runtime.core.exception.ErrorTypeRepositoryFactory;
import org.mule.runtime.core.registry.DefaultRegistryBroker;
import org.mule.runtime.core.registry.MuleRegistryHelper;
import org.mule.runtime.extension.api.annotation.Extension;
import org.mule.runtime.extension.api.loader.ExtensionModelLoader;
import org.mule.runtime.module.extension.internal.manager.DefaultExtensionManager;
import org.mule.test.runner.infrastructure.ExtensionsTestInfrastructureDiscoverer;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Paths;
import java.util.List;
import java.util.Set;
import org.eclipse.aether.artifact.Artifact;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.type.filter.AnnotationTypeFilter;
/**
* Generates the {@link Extension} manifest and DSL resources.
*
* @since 4.0
*/
class ExtensionPluginMetadataGenerator {
private static final String GENERATED_TEST_RESOURCES = "generated-test-resources";
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final ExtensionsTestInfrastructureDiscoverer extensionsInfrastructure;
private final File generatedResourcesBase;
private final File extensionMulePluginJson;
private final ExtensionModelLoaderFinder extensionModelLoaderFinder;
private List<ExtensionGeneratorEntry> extensionGeneratorEntries = newArrayList();
/**
* Creates an instance that will generated metadata for extensions on the baseResourcesFolder
*
* @param baseResourcesFolder {@link File} folder to write resources generated for each extension
*/
ExtensionPluginMetadataGenerator(File baseResourcesFolder) {
this(baseResourcesFolder, new ExtensionModelLoaderFinder());
}
ExtensionPluginMetadataGenerator(File baseResourcesFolder, ExtensionModelLoaderFinder loaderFinder) {
this.extensionsInfrastructure = new ExtensionsTestInfrastructureDiscoverer(createExtensionManager());
this.generatedResourcesBase = getGeneratedResourcesBase(baseResourcesFolder);
this.extensionMulePluginJson = getExtensionMulePluginJsonFile(baseResourcesFolder);
this.extensionModelLoaderFinder = loaderFinder;
}
private File getExtensionMulePluginJsonFile(File baseResourcesFolder) {
return Paths.get(baseResourcesFolder.getPath(), "classes", META_INF_MULE_PLUGIN).toFile();
}
/**
* Creates the {@value #GENERATED_TEST_RESOURCES} inside the target folder to put metadata files for extensions. If no exists,
* it will create it.
*
* @return {@link File} baseResourcesFolder to write extensions metadata.
*/
private File getGeneratedResourcesBase(File baseResourcesFolder) {
File generatedResourcesBase = new File(baseResourcesFolder, GENERATED_TEST_RESOURCES);
generatedResourcesBase.mkdir();
return generatedResourcesBase;
}
/**
* Creates a {@link ExtensionManager} needed for generating the metadata for an extension. It would be later discarded due to
* the manager would have references to classes loaded with the launcher class loader instead of the hierarchical class loaders
* created as result of the classification process.
*
* @return an {@link ExtensionManager} that would be used to register the extensions.
*/
private ExtensionManager createExtensionManager() {
DefaultExtensionManager extensionManager = new DefaultExtensionManager();
extensionManager.setMuleContext(new DefaultMuleContext() {
private ErrorTypeRepository errorTypeRepository = ErrorTypeRepositoryFactory.createDefaultErrorTypeRepository();
private ErrorTypeLocator errorTypeLocator = ErrorTypeLocatorFactory.createDefaultErrorTypeLocator(errorTypeRepository);
@Override
public MuleRegistry getRegistry() {
return new MuleRegistryHelper(new DefaultRegistryBroker(this), this);
}
@Override
public ErrorTypeLocator getErrorTypeLocator() {
return errorTypeLocator;
}
@Override
public ErrorTypeRepository getErrorTypeRepository() {
return errorTypeRepository;
}
});
try {
extensionManager.initialise();
} catch (InitialisationException e) {
throw new RuntimeException("Error while initialising the extension manager", e);
}
return extensionManager;
}
/**
* Scans for a {@link Class} annotated with {@link Extension} annotation and return the {@link Class} or {@code null} if there
* is no annotated {@link Class}.
*
* @param plugin the {@link Artifact} to generate its extension manifest if it is an extension.
* @param urls {@link URL}s to use for discovering {@link Class}es annotated with {@link Extension}
* @return {@link Class} annotated with {@link Extension} or {@code null}
*/
Class scanForExtensionAnnotatedClasses(Artifact plugin, List<URL> urls) {
logger.debug("Scanning plugin '{}' for annotated Extension class", plugin);
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AnnotationTypeFilter(Extension.class));
try (URLClassLoader classLoader = new URLClassLoader(urls.toArray(new URL[0]), null)) {
scanner.setResourceLoader(new PathMatchingResourcePatternResolver(classLoader));
Set<BeanDefinition> extensionsAnnotatedClasses = scanner.findCandidateComponents("");
if (!extensionsAnnotatedClasses.isEmpty()) {
if (extensionsAnnotatedClasses.size() > 1) {
logger
.warn("While scanning class loader on plugin '{}' for discovering @Extension classes annotated, more than one " +
"found. It will pick up the first one, found: {}", plugin, extensionsAnnotatedClasses);
}
String extensionClassName = extensionsAnnotatedClasses.iterator().next().getBeanClassName();
try {
return Class.forName(extensionClassName);
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException("Cannot load Extension class '" + extensionClassName + "'", e);
}
}
logger.debug("No class found annotated with {}", Extension.class.getName());
return null;
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
/**
* Discovers the extension and builds the {@link ExtensionModel}.
*
* @param plugin the extension {@link Artifact} plugin
* @param extensionClass the {@link Class} annotated with {@link Extension}
* @param dependencyResolver the dependency resolver used to introspect the artifact pom.xml
* @return {@link ExtensionModel} for the extensionClass
*/
private ExtensionModel getExtensionModel(Artifact plugin, Class extensionClass, DependencyResolver dependencyResolver) {
ExtensionModelLoader loader = extensionModelLoaderFinder.findLoaderByProperty(plugin, dependencyResolver)
.orElse(extensionModelLoaderFinder.findLoaderFromMulePlugin(extensionMulePluginJson));
return extensionsInfrastructure.discoverExtension(extensionClass, loader);
}
/**
* Generates the extension resources for the {@link Artifact} plugin with the {@link Extension}.
*
* @param plugin the {@link Artifact} to generate its extension manifest if it is an extension.
* @param extensionClass {@link Class} annotated with {@link Extension}
* @param dependencyResolver the dependency resolver used to discover test extensions poms to find which loader to use
* @return {@link File} folder where extension manifest resources were generated
*/
File generateExtensionResources(Artifact plugin, Class extensionClass, DependencyResolver dependencyResolver) {
logger.debug("Generating Extension metadata for extension class: '{}'", extensionClass);
final ExtensionModel extensionModel = getExtensionModel(plugin, extensionClass, dependencyResolver);
File generatedResourcesDirectory = new File(generatedResourcesBase, plugin.getArtifactId() + separator + "META-INF");
generatedResourcesDirectory.mkdirs();
extensionsInfrastructure.generateLoaderResources(extensionModel, generatedResourcesDirectory);
extensionGeneratorEntries.add(new ExtensionGeneratorEntry(extensionModel, generatedResourcesDirectory));
return generatedResourcesDirectory.getParentFile();
}
/**
* Generates DSL resources for the plugins where extension manifest were generated. This method should be called after all
* extensions manifest where generated.
* <p>
* <pre>
* spring.schemas
* spring.handlers
* extension.xsd
* </pre>
* <p/>
* These files are going to be generated for each extension registered here.
*/
public void generateDslResources() {
extensionGeneratorEntries.forEach(entry -> extensionsInfrastructure.generateDslResources(entry.getResourcesFolder(),
entry.getExtensionModel()));
}
/**
* Entry class for generating resources for an Extension.
*/
class ExtensionGeneratorEntry {
private ExtensionModel runtimeExtensionModel;
private File resourcesFolder;
ExtensionGeneratorEntry(ExtensionModel runtimeExtensionModel, File resourcesFolder) {
this.runtimeExtensionModel = runtimeExtensionModel;
this.resourcesFolder = resourcesFolder;
}
public ExtensionModel getExtensionModel() {
return runtimeExtensionModel;
}
File getResourcesFolder() {
return resourcesFolder;
}
}
}