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

org.technologybrewery.fermenter.mda.reporting.StatisticsService Maven / Gradle / Ivy

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

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.AbstractMavenLifecycleParticipant;
import org.apache.maven.execution.MavenSession;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.technologybrewery.fermenter.mda.GenerateSourcesMojo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 *  Records reporting data during project execution and prints the resulting report at the end of the build.
 *
 * IMPLEMENTATION NOTES:
 *  In an ideal world, this is a singleton that is only created once for the entire Maven session and is injected
 *  into each {@link GenerateSourcesMojo} that executes for each module/sub-project.  However, I was unable to figure
 *  out exactly how to pull that off given the Plexus and JSR330 annotations.  I tried the naive approach of using
 *  {@link org.apache.maven.SessionScoped} and {@link javax.inject.Singleton}.  In the latter case, the singleton is
 *  scoped to the Mojo execution and so gets created and destroyed each time.  For the former (strangely), the object
 *  is recreated even more, and the object that's created at the very start of execution is inaccessible from the
 *  context of the Mojo.  In both cases, the first instance created never records stats but is always the one to print
 *  stats. It would be nice to eventually figure out the best way to specify a bean that is created at the start of the
 *  Maven execution and is re-used throughout execution by each Mojo invoked.  Note also that the confusion is
 *  compounded by Maven's switch from Plexus to JSR330 backed by Guice while maintaining the old Plexus annotations for
 *  compatibility.
 */
@Named
@Singleton
public final class StatisticsService extends AbstractMavenLifecycleParticipant {
    private static final Logger logger = LoggerFactory.getLogger(StatisticsService.class);
    private static final String STATS_KEY = "fermenter.generationStats";
    private static final String ENABLE_STATS = "fermenter.enableStatistics";

    private final MavenSession session;
    private final String uuid;
    private boolean statsReportingEnabled;

    @Inject
    public StatisticsService(MavenSession aSession) {
        this.session = aSession;
        String enabledString = (String) session.getSystemProperties().getOrDefault(ENABLE_STATS, "false");
        statsReportingEnabled = "true".equalsIgnoreCase(enabledString);
        uuid = UUID.randomUUID().toString();
    }

    @Override
    public void afterSessionEnd(MavenSession session) {
        if (!session.getResult().hasExceptions() && statsReportingEnabled) {
            FileStats.Aggregate totals = calculateFinalStats();
            logger.info("****************************************************************************");
            logger.info("*                        Fermenter Execution Report                        *");
            logger.info("*");
            logger.info("*   Files generated:            " + totals.getFileCount() + " files");
            logger.info("*   Size of generated files:    " + totals.getTotalSize() + " bytes");
            logger.info("*   Lines generated :           " + totals.getTotalLines() + " lines");
            logger.info("*");
            logger.info("*");
            logger.info("* Note: to find the real size of your project in bytes");
            logger.info("*   1. Delete build artifacts (e.g. `mvn clean`)");
            logger.info("*   2. Delete any package lock files (e.g. `find . -name poetry.lock -delete`)");
            logger.info("*   3. Run `ls -lR | grep -E '^-' | awk '{sum+=$5;} END{print sum}'`");
            logger.info("****************************************************************************");
        }
    }

    /**
     * Record a file that's being generated or would be generated by the generate-sources Mojo. Files that are not
     * generated simply because they were already generated and have overwritable set to false are still recorded.
     *
     * @param template the velocity template used to generate the file
     * @param destinationFile the final destination of the generated file
     * @param vc the velocity context for generation
     */
    public void recordStats(Template template, File destinationFile, VelocityContext vc) {
        if (isStatsReportingEnabled()) {
            StatsCollectingWriter fw = new StatsCollectingWriter(this, destinationFile);
            template.merge(vc, fw);
            fw.close();
        }
    }

    /**
     * Whether the reporting service is reporting on source generation statistics
     *
     * @return true if stats are being recorded
     */
    public boolean isStatsReportingEnabled() {
        return statsReportingEnabled;
    }

    /**
     * Enables or disables reporting on source generation statistics
     *
     * @param statsReportingEnabled whether stats are enabled
     */
    public void setStatsReportingEnabled(boolean statsReportingEnabled) {
        this.statsReportingEnabled = statsReportingEnabled;
    }

    /**
     * Adds recorded stats for a specific file to the total stats gathered for the session
     *
     * @param newStats the newly recorded stats
     */
    synchronized void updateGeneratedFileStats(FileStats newStats) {
        // NB: It would be much more efficient to just store the aggregated numbers, however currently python source
        // files appear to be generated twice: once during generate-python-sources and once during generate-sources.
        // this also defends against inaccurate details for poor Fermenter implementations that have duplicate
        // targets/output files.
        List allStats = readStatsFromSession(STATS_KEY + uuid);
        if (!allStats.contains(newStats)) {
            allStats.add(newStats);
        } else {
            logger.warn("Duplicate generation for " + newStats.getFilePath());
        }
        writeStatsToSession(STATS_KEY + uuid, allStats);
    }

    /**
     * Reads all the stats stored in the session (across all keys) and aggregates them.
     *
     * @return aggregated stats
     */
    FileStats.Aggregate calculateFinalStats() {
        List allStats = new ArrayList<>();
        for (Object eachKey : session.getUserProperties().keySet()) {
            if (eachKey instanceof String && ((String) eachKey).startsWith(STATS_KEY)) {
                allStats.addAll(readStatsFromSession((String) eachKey));
            }
        }
        return FileStats.aggregate(allStats);
    }

    /**
     * Reads the stats that have been persisted to the Maven session
     *
     * @param key the key where the stats are stored in the session
     *
     * @return the stats currently stored in the Maven session
     */
    private List readStatsFromSession(String key) {
        List sessionStats = new ArrayList<>();
        try {
            final ObjectMapper mapper = new ObjectMapper();
            final String json = (String) session.getUserProperties().get(key);
            if (!StringUtils.isEmpty(json)) {
                sessionStats = mapper.readValue(json, new TypeReference<>() {});
            }
        } catch (JsonProcessingException e) {
            if (isStatsReportingEnabled()) {
                throw new RuntimeException("Unable to get generation stats from maven session", e);
            }
        }
        return sessionStats;
    }

    /**
     * Persists the statistics recorded thus far to the Maven session
     *
     * @param key the key where the stats should be stored in the session
     * @param stats the stats to store in the session
     */
    private void writeStatsToSession(String key, List stats) {
        try {
            final ObjectMapper mapper = new ObjectMapper();
            String json = mapper.writeValueAsString(stats);
            session.getUserProperties().setProperty(key, json);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("Failed to persist updated generation statistics", e);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy