All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.technologybrewery.fermenter.mda.GenerateSourcesMojo Maven / Gradle / Ivy

The newest version!
package org.technologybrewery.fermenter.mda;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.LogFactory;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.versioning.ComparableVersion;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Plugin;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
import org.technologybrewery.fermenter.mda.element.ExpandedFamily;
import org.technologybrewery.fermenter.mda.element.ExpandedProfile;
import org.technologybrewery.fermenter.mda.element.Family;
import org.technologybrewery.fermenter.mda.element.Profile;
import org.technologybrewery.fermenter.mda.element.Target;
import org.technologybrewery.fermenter.mda.generator.GenerationContext;
import org.technologybrewery.fermenter.mda.generator.Generator;
import org.technologybrewery.fermenter.mda.metamodel.ModelInstanceRepository;
import org.technologybrewery.fermenter.mda.metamodel.ModelInstanceUrl;
import org.technologybrewery.fermenter.mda.metamodel.ModelRepositoryConfiguration;
import org.technologybrewery.fermenter.mda.notification.NotificationService;
import org.technologybrewery.fermenter.mda.reporting.StatisticsService;
import org.technologybrewery.fermenter.mda.util.MessageTracker;

import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

/**
 * Executes the Fermenter MDA process.
 */
@Mojo(name = "generate-sources", defaultPhase = LifecyclePhase.GENERATE_SOURCES, requiresDependencyResolution = ResolutionScope.COMPILE, threadSafe = true)
public class GenerateSourcesMojo extends AbstractMojo {

    private static final org.apache.commons.logging.Log LOG = LogFactory.getLog(GenerateSourcesMojo.class);

    private Map profiles = new HashMap<>();

    private Map targets = new HashMap<>();

    private Map families = new HashMap<>();

    @Inject
    private StatisticsService statisticsService;

    @Inject
    private NotificationService notificationService;

    @Parameter(required = true, readonly = true, defaultValue = "${project}")
    private MavenProject project;

    @Parameter(defaultValue = "${plugin}", readonly = true)
    private PluginDescriptor plugin;

    @Parameter(required = true)
    private String profile;

    @Parameter
    private List metadataDependencies;

    @Parameter
    private String basePackage;

    @Parameter
    private List suppressedMessages;

    /**
     * Captures the target programming language in which source code artifacts will be generated. This
     * configuration drives the automatic configuration of {@link #mainSourceRoot}, {@link #generatedSourceRoot},
     * {@link #generatedTestSourceRoot}, and other language specific structural elements.
     */
    @Parameter(required = true, defaultValue = "java")
    private String language;

    private String targetsFileLocation = "targets.json";
    private String profilesFileLocation = "profiles.json";
    private String familiesFileLocation = "families.json";

    /**
     * Represents the artifactIds that will be targeted for generation if the
     * metadataContext equals 'targeted'.
     */
    @Parameter(required = false)
    private List targetModelInstances;

    @Parameter(required = true, defaultValue = "${project.basedir}/src/main")
    private File mainSourceRoot;

    @Parameter(required = true, defaultValue = "${project.basedir}/src/generated")
    private File generatedSourceRoot;

    @Parameter(required = true, defaultValue = "${project.basedir}/src/test")
    private File testSourceRoot;

    @Parameter(required = true, defaultValue = "${project.basedir}/src/generated-test")
    private File generatedTestSourceRoot;

    /**
     * Local path from which metadata will be loaded. Defaults to "{@link #mainSourceRoot}/resources"
     * via {@link #getLocalMetadataRoot()}.
     */
    @Parameter
    private File localMetadataRoot;

    @Parameter(required = true, defaultValue = "${project.basedir}/src/main/resources/types.json")
    private File localTypes;

    @Parameter(required = true, defaultValue = "org.technologybrewery.fermenter.mda.metamodel.DefaultModelInstanceRepository")
    private String metadataRepositoryImpl;

    /**
     * List of general properties to pass to the {@link GenerationContext}.
     */
    @Parameter
    private Map propertyVariables;

    private VelocityEngine engine;

    @Parameter(property = "session", required = true, readonly = true)
    protected MavenSession session;

    /**
     * Creates a {@link GenerateSourcesHelper.LoggerDelegate} implementation for use with
     * {@link GenerateSourcesHelper} that delegates to the {@link Log} that has
     * been injected into this mojo.
     */
    protected GenerateSourcesHelper.LoggerDelegate mavenLoggerDelegate = new GenerateSourcesHelper.LoggerDelegate() {

        @Override
        public void log(LogLevel level, String message) {
            Log log = getLog();
            switch (level) {
                case TRACE:
                case DEBUG:
                    log.debug(message);
                    break;
                case INFO:
                    log.info(message);
                    break;
                case WARN:
                    log.warn(message);
                    break;
                case ERROR:
                    log.error(message);
                    break;
                default:
                    log.info(message);
                    break;
            }
        }
    };

    @Override
    public void execute() throws MojoExecutionException {
        GenerateSourcesHelper.suppressKrauseningWarnings();

        try {
            setup();
            GenerateSourcesHelper.performSourceGeneration(profile, profiles, this::createGenerationContext,
                this::handleInvalidProfile, mavenLoggerDelegate, project.getBasedir());
        } catch (Exception e) {
            String message = "Error while performing source generation";
            // NB logging and re-throwing isn't usually a best practice as it
            // can result in duplicative error logging and clutter, but here it
            // can be helpful in providing additional context about an error
            // without needing to re-run Maven with -e or -X turned on
            getLog().error(message, e);
            throw new MojoExecutionException(message, e);

        } finally {
            GenerateSourcesHelper.cleanUp();

        }

        // store notifications in the target directory between plugin invocations so they can be output
        // at the end of the build:
        notificationService.recordNotifications(getProject(), suppressedMessages);

    }

    /**
     * Performs all setup activities required to load and validate metamodels
     * prior to code generation, including loading generation targets and
     * profiles, automatically adding src/generated/java to the project's list
     * of source directories, and loading/validating metamodels into the
     * appropriate {@link ModelInstanceRepository}.
     *
     * @throws MojoExecutionException any unexpected error occurs during metamodel loading and
     *                                validation.
     */
    private void setup() throws MojoExecutionException {
        if (metadataDependencies == null) {
            metadataDependencies = new ArrayList<>();
        }

        updateMojoConfigsBasedOnLanguage();
        validateMojoConfigs();

        loadTargets();
        loadProfiles();
        loadFamilies();
        TypeManager.getInstance().loadLocalTypes(localTypes);

        if (isGeneratingJavaProject()) {
            try {
                project.addCompileSourceRoot(getJavaCompilePathForGeneratedSource());
            } catch (IOException e) {
                throw new MojoExecutionException("Could not add generated Java source root to project compilation path list", e);
            }
        }

        try {
            ModelRepositoryConfiguration config = createMetadataConfiguration();
            ModelInstanceRepository newRepository = GenerateSourcesHelper.loadMetamodelRepository(config,
                metadataRepositoryImpl, mavenLoggerDelegate);

            GenerateSourcesHelper.validateMetamodelRepository(newRepository, mavenLoggerDelegate);
        } catch (MalformedURLException | ClassNotFoundException | NoSuchMethodException | InstantiationException
                 | IllegalAccessException | InvocationTargetException e) {
            throw new MojoExecutionException("Could not successfully load metamodel repository", e);
        }

        engine = new VelocityEngine();
        engine.setProperty(RuntimeConstants.RESOURCE_LOADERS, "classpath");
        engine.setProperty("resource.loader.classpath.class", ClasspathResourceLoader.class.getName());
        engine.init();
    }


    /**
     * Scans the classpath for any families.json files and loads all defined
     * {@link Family} configurations.
     *
     * @throws MojoExecutionException
     */
    public void loadFamilies() throws MojoExecutionException {
        Enumeration familyEnumeration = null;
        try {
            familyEnumeration = getClass().getClassLoader().getResources(familiesFileLocation);
        } catch (IOException ioe) {
            throw new MojoExecutionException("Unable to find families", ioe);
        }

        URL familiesResource;
        while (familyEnumeration.hasMoreElements()) {
            familiesResource = familyEnumeration.nextElement();
            getLog().info(String.format("Loading families from: %s", familiesResource.toString()));

            try (InputStream familiesStream = familiesResource.openStream()) {
                families = GenerateSourcesHelper.loadFamilies(familiesStream, families);
            } catch (IOException e) {
                throw new MojoExecutionException("Unable to parse " + familiesFileLocation, e);
            }
        }

        for (ExpandedFamily f : families.values()) {
            f.dereference(families, profiles);
        }

    }

    /**
     * Scans the classpath for any targets.json files and loads all defined
     * {@link Target} configurations.
     *
     * @throws MojoExecutionException
     */
    public void loadTargets() throws MojoExecutionException {
        Enumeration targetEnumeration = null;
        try {
            targetEnumeration = getClass().getClassLoader().getResources(targetsFileLocation);
        } catch (IOException ioe) {
            throw new MojoExecutionException("Unable to find targets", ioe);
        }

        URL targetsResource;
        while (targetEnumeration.hasMoreElements()) {
            targetsResource = targetEnumeration.nextElement();
            getLog().info(String.format("Loading targets from: %s", targetsResource.toString()));

            try (InputStream targetsStream = targetsResource.openStream()) {
                targets = GenerateSourcesHelper.loadTargets(targetsStream, targets);
            } catch (IOException e) {
                throw new MojoExecutionException("Unable to parse " + targetsFileLocation, e);
            }
        }
    }

    /**
     * Scans the classpath for any profiles.json files and loads all defined
     * {@link Profile} configurations.
     *
     * @throws MojoExecutionException
     */
    public void loadProfiles() throws MojoExecutionException {
        Enumeration profileEnumeration = null;
        try {
            profileEnumeration = getClass().getClassLoader().getResources(profilesFileLocation);
        } catch (IOException ioe) {
            throw new MojoExecutionException("Unable to find profiles", ioe);
        }

        URL profilesResource;
        while (profileEnumeration.hasMoreElements()) {
            profilesResource = profileEnumeration.nextElement();
            getLog().info(String.format("Loading profiles from: %s", profilesResource.toString()));

            try (InputStream profilesStream = profilesResource.openStream()) {
                profiles = GenerateSourcesHelper.loadProfiles(profilesStream, profiles);
            } catch (IOException e) {
                throw new MojoExecutionException("Unable to parse " + profilesFileLocation, e);
            }
        }

        for (ExpandedProfile p : profiles.values()) {
            p.dereference(profiles, targets);
        }
    }

    /**
     * Creates a {@link ModelRepositoryConfiguration} utilized by the
     * appropriate {@link ModelInstanceRepository} for metamodel loading. This
     * method primarily extracts the metamodel dependencies specified in the
     * <metadataDependencies> and <targetModelInstances>
     * configurations and enables them to be appropriately referenced through
     * the created {@link ModelRepositoryConfiguration}.
     *
     * @return appropriately configured {@link ModelRepositoryConfiguration}
     * that may be used for metamodel processing.
     * @throws MalformedURLException
     */
    private ModelRepositoryConfiguration createMetadataConfiguration() throws MalformedURLException {
        ModelRepositoryConfiguration config = new ModelRepositoryConfiguration();
        config.setArtifactId(project.getArtifactId());
        config.setBasePackage(basePackage);

        List targetedArtifactIds = new ArrayList<>();
        if (CollectionUtils.isEmpty(targetModelInstances)) {
            targetedArtifactIds.add(project.getArtifactId());

        } else {
            targetedArtifactIds = targetModelInstances;

        }

        if ((targetedArtifactIds.size() > 1) || (!targetedArtifactIds.contains(project.getArtifactId()))) {
            LOG.info("Generation targets (" + targetedArtifactIds.size()
                + ") are different from project's local metadata (" + targetedArtifactIds.toString() + ")");
        }

        config.setTargetModelInstances(targetedArtifactIds);
        Map metadataUrls = config.getMetamodelInstanceLocations();
        String projectUrl = getLocalMetadataRoot().toURI().toURL().toString();
        metadataUrls.put(project.getArtifactId(), new ModelInstanceUrl(project.getArtifactId(), projectUrl));
        PackageManager.addMapping(project.getArtifactId(), basePackage);

        if (metadataDependencies != null) {
            metadataDependencies.add(project.getArtifactId());

            List artifacts = plugin.getArtifacts();
            for (Artifact a : artifacts) {
                if (metadataDependencies.contains(a.getArtifactId())) {
                    URL url = a.getFile().toURI().toURL();
                    metadataUrls.put(a.getArtifactId(), new ModelInstanceUrl(a.getArtifactId(), url.toString()));
                    PackageManager.addMapping(a.getArtifactId(), url, a.getGroupId());
                    LOG.info("Adding metadataDependency to current set of metadata: " + a.getArtifactId());
                }
            }

        }
        return config;
    }

    /**
     * Helper method that automatically updates appropriate Mojo configurations based on the specified target language,
     * which is configured via {@link #language}. Specifically, this method overrides the various file locations in
     * which generated code is placed to align with the expected file structures required by each language associated
     * DevOps and build tooling.
     */
    protected void updateMojoConfigsBasedOnLanguage() {
        if (isGeneratingPythonProject()) {
            String pythonPackageFolder = getPythonPackageFolderForProject();
            this.mainSourceRoot = new File(project.getBasedir(), String.format("src/%s", pythonPackageFolder));
            this.generatedSourceRoot = new File(project.getBasedir(), String.format("src/%s/generated", pythonPackageFolder));
            this.testSourceRoot = new File(project.getBasedir(), "tests");
            this.generatedTestSourceRoot = new File(project.getBasedir(), "tests");
            this.localTypes = new File(mainSourceRoot, "resources/types.json");
            if (StringUtils.isEmpty(this.basePackage)) {
                this.basePackage = pythonPackageFolder;
            }
        }
    }

    /**
     * Performs complex validation on user-provided configurations to this Mojo, outside of metamodel validation.
     *
     * @throws MojoExecutionException unrecoverable validation error was detected.
     */
    protected void validateMojoConfigs() throws MojoExecutionException {
        MessageTracker messageTracker = MessageTracker.getInstance();
        if (isGeneratingJavaProject()) {
            if (StringUtils.isEmpty(basePackage)) {
                messageTracker.addErrorMessage(" must be specified for Java-based projects");
            }
        }

        if (messageTracker.hasErrors()) {
            messageTracker.emitMessages(mavenLoggerDelegate);
            throw new MojoExecutionException("Provided configuration was invalid!");
        }
    }

    /**
     * Handles the specification of an invalid or non-existent generation
     * profile by providing diagnostic error logging and returning the
     * appropriate exception to throw.
     *
     * @param targetProfile invalid profile specified in the fermenter-mda plugin
     *                      declaration.
     * @param allProfiles   all valid {@link Profile}s based on the fermenter-mda plugin
     *                      configuration.
     * @return a {@link MojoExecutionException} to throw and halt the build.
     */
    private Exception handleInvalidProfile(String targetProfile, Collection allProfiles) {
        StringBuilder sb = new StringBuilder();
        Set orderedProfiles = new TreeSet<>(allProfiles);
        for (ExpandedProfile profileValue : orderedProfiles) {
            sb.append("\t- ").append(profileValue.getName()).append("\n");
        }

        getLog().error("\n\n"
            + "\torg.technologybrewery.fermenter\n"
            + "\tfermenter-mda\n"
            + "\t...\n"
            + "\t\n"
            + "\t\t" + targetProfile + "   <-----------  INVALID PROFILE!\n"
            + "\t\t...\n"
            + "\t\n"
            + "\n"
            + "Profile '" + targetProfile + "' is invalid.  Please choose one of the following valid profiles:\n" + sb.toString());

        return new MojoExecutionException("Invalid profile specified: '" + targetProfile + "'");
    }

    /**
     * Creates a new {@link GenerationContext} object based on the given
     * {@link Target} which captures key configuration details needed to
     * generate the source file(s) modeled by the given {@link Target} and it's
     * {@link Generator}.
     *
     * @param target generation {@link Target} being processed.
     * @return {@link GenerationContext} that can be provided to the given
     * {@link Target}'s {@link Generator} to execute code generation.
     */
    protected GenerationContext createGenerationContext(Target target) {
        GenerationContext context = new GenerationContext(target);
        context.setStatisticsService(statisticsService);
        context.setBasePackage(basePackage);
        context.setProjectDirectory(project.getBasedir());
        context.setGeneratedSourceDirectory(generatedSourceRoot);
        context.setMainSourceDirectory(mainSourceRoot);
        context.setTestSourceDirectory(testSourceRoot);
        context.setGeneratedTestSourceDirectory(generatedTestSourceRoot);
        context.setEngine(engine);
        context.setGroupId(project.getGroupId());
        context.setArtifactId(project.getArtifactId());
        context.setVersion(project.getVersion());
        context.setDescriptiveName(project.getName());
        if (project.getScm() != null) {
            context.setScmUrl(project.getScm().getUrl());
        }
        context.setPropertyVariables(propertyVariables);
        context.setExecutionRootDirectory(new File (session.getExecutionRootDirectory()));

        String rootArtifactId = getRootArtifactId();
        context.setRootArtifactId(rootArtifactId);

        return context;
    }

    /**
     * Boolean value indicating whether this Mojo is being invoked to generate a Java-based project.
     *
     * @return
     */
    protected boolean isGeneratingJavaProject() {
        return "java".equalsIgnoreCase(this.language) && !"habushu".equals(this.getProject().getPackaging());
    }

    /**
     * Boolean value indicating whether this Mojo is being invoked to generate a Python-based project.
     * This may be explicitly specified via the {@link #language} configuration or if it is detected
     * that the project uses a recent version of the Habushu Maven plugin.
     *
     * @return
     */
    protected boolean isGeneratingPythonProject() {
        return "python".equalsIgnoreCase(this.language)
            || ("habushu".equals(this.project.getPackaging()) && !isLegacyHabushuProject());
    }

    /**
     * Boolean value indicating whether this Mojo is being invoked on a project that is a legacy
     * Habushu project.  More specifically, the determination is based on if a version of the
     * {@code org.technologybrewery.habushu:habushu-maven-plugin} with a version less than {@code 2.0.0}
     * is included in the POM's {@code }.
     *
     * @return
     */
    protected boolean isLegacyHabushuProject() {
        Plugin habushuMavenPlugin = this.project.getPlugin("org.bitbucket.cpointe.habushu:habushu-maven-plugin");
        if (habushuMavenPlugin != null) {
            ComparableVersion habushuVersion = new ComparableVersion(habushuMavenPlugin.getVersion());
            return habushuVersion.compareTo(new ComparableVersion("2.0.0")) < 0;
        }
        return false;
    }

    /**
     * Gets the normalized package folder name of the relevant Python project being generated that aligns with
     * PEP-8 naming conventions (dashes may be in published package names, but in the actual package folder hierarchy,
     * dashes should be replaced with underscores).
     *
     * @return
     */
    protected String getPythonPackageFolderForProject() {
        return StringUtils.replace(this.getProject().getArtifactId(), "-", "_");
    }

    /**
     * Retrieves the compile path at which generated Java source files will be placed. 
* * Pre-condition: The target project being generated is Java-based * * @return * @throws IOException */ protected String getJavaCompilePathForGeneratedSource() throws IOException { return new File(this.generatedSourceRoot, "java").getCanonicalPath(); } /** * Retrieves the location within this project from which local metadata definitions will be loaded. * If {@link #localMetadataRoot} is not configured, this will default to "{@link #mainSourceRoot}/resources" * (i.e. {@code src/main/resources}). * * @return */ protected File getLocalMetadataRoot() { return localMetadataRoot != null ? localMetadataRoot : new File(mainSourceRoot, "resources"); } public Map getFamilies() { return families; } public Map getProfiles() { return profiles; } public Map getTargets() { return targets; } public String getProfile() { return profile; } public void setProfile(String profile) { this.profile = profile; } public String getBasePackage() { return basePackage; } public void setBasePackage(String basePackage) { this.basePackage = basePackage; } public Map getPropertyVariables() { return propertyVariables; } public void setPropertyVariables(Map propertyVariables) { this.propertyVariables = propertyVariables; } public String getTargetsFileLocation() { return targetsFileLocation; } public void setTargetsFileLocation(String targetsFileLocation) { this.targetsFileLocation = targetsFileLocation; } public String getProfilesFileLocation() { return profilesFileLocation; } public void setProfilesFileLocation(String profilesFileLocation) { this.profilesFileLocation = profilesFileLocation; } public MavenProject getProject() { return project; } protected File getMainSourceRoot() { return mainSourceRoot; } protected File getGeneratedSourceRoot() { return generatedSourceRoot; } public String getRootArtifactId() { MavenProject topLevelProject = session.getTopLevelProject(); String rootArtifactId = topLevelProject.getArtifactId(); return rootArtifactId; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy