001package com.labun.buildnumber;
002
003import static com.labun.buildnumber.BuildNumberExtractor.propertyNames;
004
005import java.io.File;
006import java.util.Arrays;
007import java.util.List;
008import java.util.Map;
009import java.util.Objects;
010import java.util.Properties;
011import java.util.TreeMap;
012
013import org.apache.maven.plugin.AbstractMojo;
014import org.apache.maven.plugin.MojoExecutionException;
015import org.apache.maven.plugin.MojoFailureException;
016import org.apache.maven.plugins.annotations.Component;
017import org.apache.maven.plugins.annotations.LifecyclePhase;
018import org.apache.maven.plugins.annotations.Mojo;
019import org.apache.maven.plugins.annotations.Parameter;
020import org.apache.maven.project.MavenProject;
021import org.sonatype.plexus.build.incremental.BuildContext;
022
023import lombok.Getter;
024import lombok.Setter;
025
026/** Extracts Git metadata and creates build number. Publishes them as project properties. */
027@Getter
028@Setter
029@Mojo(name = "extract-buildnumber", defaultPhase = LifecyclePhase.VALIDATE, threadSafe = true)
030public class JGitBuildNumberMojo extends AbstractMojo implements Parameters {
031
032    @Component
033    private BuildContext buildContext;
034
035    // ---------- parameters (user configurable) ----------
036
037    private @Parameter String namespace;
038    private @Parameter String dirtyValue;
039    private @Parameter Integer shortRevisionLength;
040    private @Parameter String gitDateFormat;
041    private @Parameter String buildDateFormat;
042    private @Parameter String dateFormatTimeZone;
043    private @Parameter String countCommitsSinceInclusive;
044    private @Parameter String countCommitsSinceExclusive;
045    private @Parameter String countCommitsInPath;
046    private @Parameter String buildNumberFormat;
047    private @Parameter File repositoryDirectory;
048    private @Parameter Boolean runOnlyAtExecutionRoot;
049    private @Parameter Boolean skip;
050    private @Parameter Boolean verbose;
051
052    // ---------- parameters (read only) ----------
053
054    @Parameter(property = "project.basedir", readonly = true, required = true)
055    private File baseDirectory;
056
057    @Parameter(property = "session.executionRootDirectory", readonly = true, required = true)
058    private File executionRootDirectory;
059
060    /** The maven project. */
061    @Parameter(property = "project", readonly = true)
062    private MavenProject project;
063
064    /** The maven parent project. */
065    @Parameter(property = "project.parent", readonly = true)
066    private MavenProject parentProject;
067
068    // ---------- implementation ----------
069
070    /** Extracts buildnumber fields from git repository and publishes them as maven properties.
071     *  Executes only once per build. Return default (unknown) buildnumber fields on error. */
072    @Override
073    public void execute() throws MojoExecutionException, MojoFailureException {
074        long start = System.currentTimeMillis();
075
076        // set some parameters to Maven specific values
077        if (getRepositoryDirectory() == null) setRepositoryDirectory(project.getBasedir()); // ${project.basedir}
078
079        validateAndSetParameterValues();
080
081        if (skip) {
082            getLog().info("Execution is skipped by configuration.");
083            return;
084        }
085
086        if (verbose) getLog().info("JGit BuildNumber Maven Plugin - start");
087        if (verbose) getLog().info("executionRootDirectory: " + executionRootDirectory + ", baseDirectory: " + baseDirectory);
088
089        try {
090            // accesses Git repo only once per build
091            // http://www.sonatype.com/people/2009/05/how-to-make-a-plugin-run-once-during-a-build/
092            if (!runOnlyAtExecutionRoot || executionRootDirectory.equals(baseDirectory)) {
093
094                BuildNumberExtractor extractor = new BuildNumberExtractor(this, msg -> getLog().info(msg));
095
096                String headSha1 = extractor.getHeadSha1();
097                String dirty = extractor.isGitStatusDirty() ? dirtyValue : null;
098
099                List<Object> params = Arrays.asList(headSha1, dirty, shortRevisionLength, gitDateFormat, buildDateFormat, dateFormatTimeZone,
100                    countCommitsSinceInclusive, countCommitsSinceExclusive, countCommitsInPath, buildNumberFormat);
101                String paramsKey = "jgitParams" + namespace;
102                String resultKey = "jgitResult" + namespace;
103
104                // note: saving/loading custom classes doesn't work (due to different classloaders?, "cannot be cast" error);
105                // when saving Properties object, our values don't survive; therefore we use a Map here
106                Map<String, String> result = getCachedResultFromBuildConext(paramsKey, params, resultKey);
107                if (result != null) {
108                    if (verbose) getLog().info("using cached result: " + result);
109                } else {
110                    result = extractor.extract();
111                    saveResultToBuildContext(paramsKey, params, resultKey, result);
112                }
113                setProperties(result, project.getProperties());
114
115            } else if ("pom".equals(parentProject.getPackaging())) {
116                // build started from parent, we are in subproject, lets provide parent properties to our project
117                Properties parentProps = parentProject.getProperties();
118                String revision = parentProps.getProperty(namespace + "." + "revision");
119                if (revision == null) {
120                    // we are in subproject, but parent project wasn't build this time,
121                    // maybe build is running from parent with custom module list - 'pl' argument
122                    getLog().warn("Cannot extract Git info, maybe custom build with 'pl' argument is running");
123                    fillPropsUnknown(); // TODO: throw exception instead?
124                    return;
125                }
126                if (verbose) getLog().info("using already extracted properties from parent module: " + toMap(parentProps));
127                setProperties(parentProps, project.getProperties());
128
129            } else {
130                // should not happen
131                getLog().warn("Cannot extract JGit version: something wrong with build process, we're not in parent, not in subproject!");
132                fillPropsUnknown(); // TODO: throw exception instead?
133            }
134        } catch (Exception e) {
135            String message = e.getMessage() != null ? e.getMessage() : /* e.g. NPE */ e.getClass().getSimpleName();
136            getLog().error(message);
137            // if (verbose) getLog().error(e); // stacktrace (can be printed by Maven with debug output)
138            // fillPropsUnknown();
139            throw new MojoFailureException(message, e);
140        } finally {
141            long duration = System.currentTimeMillis() - start;
142            if (verbose) getLog().info(String.format("JGit BuildNumber Maven Plugin - end (execution time: %d ms)", duration));
143        }
144    }
145
146    // m2e build? => save extracted values to BuildContext
147    private void saveResultToBuildContext(String paramsKey, List<Object> currentParams, String resultKey, Map<String, String> result) {
148        if (buildContext != null) {
149            buildContext.setValue(paramsKey, currentParams);
150            buildContext.setValue(resultKey, result);
151        }
152    }
153
154    // m2e incremental build and input params (HEAD, etc.) not changed? => try to get previously extracted values from BuildContext
155    // note: buildContext != null only in m2e builds in Eclipse
156    private Map<String, String> getCachedResultFromBuildConext(String paramsKey, List<Object> currentParams, String resultKey) {
157        if (buildContext != null && buildContext.isIncremental()) {
158            if (verbose) getLog().info("m2e incremental build detected");
159            // getLog().info("buildContext.getClass(): " + buildContext.getClass()); // org.eclipse.m2e.core.internal.embedder.EclipseBuildContext
160            List<Object> cachedParams = (List<Object>) buildContext.getValue(paramsKey);
161            // getLog().info("cachedParams: " + cachedParams);
162            if (Objects.equals(cachedParams, currentParams)) {
163                Map<String, String> cachedResult = (Map<String, String>) buildContext.getValue(resultKey);
164                // getLog().info("cachedResult: " + cachedResult);
165                return cachedResult;
166            }
167        }
168        return null;
169    }
170
171    private Map<String, String> toMap(Properties props) {
172        Map<String, String> map = new TreeMap<>();
173        for (String propertyName : propertyNames)
174            map.put(propertyName, props.getProperty(namespace + "." + propertyName));
175
176        return map;
177    }
178
179    private void setProperties(Map<String, String> source, Properties target) {
180        for (Map.Entry<String, String> e : source.entrySet())
181            target.setProperty(namespace + "." + e.getKey(), e.getValue());
182    }
183
184    private void setProperties(Properties source, Properties target) {
185        for (String propertyName : propertyNames) {
186            String prefixedName = namespace + "." + propertyName;
187            target.setProperty(prefixedName, source.getProperty(prefixedName));
188        }
189    }
190
191    private void fillPropsUnknown() {
192        Properties props = project.getProperties();
193        for (String propertyName : propertyNames)
194            props.setProperty(namespace + "." + propertyName, "UNKNOWN-" + propertyName);
195    }
196}