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);
}
}
}