/* * Copyright 2012 The Apache Software Foundation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.hadoop.maven.plugin.versioninfo; import org.apache.hadoop.maven.plugin.util.Exec; import org.apache.hadoop.maven.plugin.util.FileSetUtils; import org.apache.maven.model.FileSet; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; 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.project.MavenProject; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.TimeZone; /** * VersionInfoMojo calculates information about the current version of the * codebase and exports the information as properties for further use in a Maven * build. The version information includes build time, SCM URI, SCM branch, SCM * commit, and an MD5 checksum of the contents of the files in the codebase. */ @Mojo(name="version-info") public class VersionInfoMojo extends AbstractMojo { @Parameter(defaultValue="${project}") private MavenProject project; @Parameter(required=true) private FileSet source; @Parameter(defaultValue="version-info.build.time") private String buildTimeProperty; @Parameter(defaultValue="version-info.source.md5") private String md5Property; @Parameter(defaultValue="version-info.scm.uri") private String scmUriProperty; @Parameter(defaultValue="version-info.scm.branch") private String scmBranchProperty; @Parameter(defaultValue="version-info.scm.commit") private String scmCommitProperty; @Parameter(defaultValue="git") private String gitCommand; @Parameter(defaultValue="svn") private String svnCommand; private enum SCM {NONE, SVN, GIT} @Override public void execute() throws MojoExecutionException { try { SCM scm = determineSCM(); project.getProperties().setProperty(buildTimeProperty, getBuildTime()); project.getProperties().setProperty(scmUriProperty, getSCMUri(scm)); project.getProperties().setProperty(scmBranchProperty, getSCMBranch(scm)); project.getProperties().setProperty(scmCommitProperty, getSCMCommit(scm)); project.getProperties().setProperty(md5Property, computeMD5()); } catch (Throwable ex) { throw new MojoExecutionException(ex.toString(), ex); } } /** * Returns a string representing current build time. * * @return String representing current build time */ private String getBuildTime() { DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'"); dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); return dateFormat.format(new Date()); } private List<String> scmOut; /** * Determines which SCM is in use (Subversion, git, or none) and captures * output of the SCM command for later parsing. * * @return SCM in use for this build * @throws Exception if any error occurs attempting to determine SCM */ private SCM determineSCM() throws Exception { Exec exec = new Exec(this); SCM scm = SCM.NONE; scmOut = new ArrayList<String>(); int ret = exec.run(Arrays.asList(svnCommand, "info"), scmOut); if (ret == 0) { scm = SCM.SVN; } else { ret = exec.run(Arrays.asList(gitCommand, "branch"), scmOut); if (ret == 0) { ret = exec.run(Arrays.asList(gitCommand, "remote", "-v"), scmOut); if (ret != 0) { scm = SCM.NONE; scmOut = null; } else { ret = exec.run(Arrays.asList(gitCommand, "log", "-n", "1"), scmOut); if (ret != 0) { scm = SCM.NONE; scmOut = null; } else { scm = SCM.GIT; } } } } if (scmOut != null) { getLog().debug(scmOut.toString()); } getLog().info("SCM: " + scm); return scm; } /** * Return URI and branch of Subversion repository. * * @param str String Subversion info output containing URI and branch * @return String[] containing URI and branch */ private String[] getSvnUriInfo(String str) { String[] res = new String[]{"Unknown", "Unknown"}; try { String path = str; int index = path.indexOf("trunk"); if (index > -1) { res[0] = path.substring(0, index - 1); res[1] = "trunk"; } else { index = path.indexOf("branches"); if (index > -1) { res[0] = path.substring(0, index - 1); int branchIndex = index + "branches".length() + 1; index = path.indexOf("/", branchIndex); if (index > -1) { res[1] = path.substring(branchIndex, index); } else { res[1] = path.substring(branchIndex); } } } } catch (Exception ex) { getLog().warn("Could not determine URI & branch from SVN URI: " + str); } return res; } /** * Parses SCM output and returns URI of SCM. * * @param scm SCM in use for this build * @return String URI of SCM */ private String getSCMUri(SCM scm) { String uri = "Unknown"; switch (scm) { case SVN: for (String s : scmOut) { if (s.startsWith("URL:")) { uri = s.substring(4).trim(); uri = getSvnUriInfo(uri)[0]; break; } } break; case GIT: for (String s : scmOut) { if (s.startsWith("origin") && s.endsWith("(fetch)")) { uri = s.substring("origin".length()); uri = uri.substring(0, uri.length() - "(fetch)".length()); break; } } break; } return uri.trim(); } /** * Parses SCM output and returns commit of SCM. * * @param scm SCM in use for this build * @return String commit of SCM */ private String getSCMCommit(SCM scm) { String commit = "Unknown"; switch (scm) { case SVN: for (String s : scmOut) { if (s.startsWith("Revision:")) { commit = s.substring("Revision:".length()); break; } } break; case GIT: for (String s : scmOut) { if (s.startsWith("commit")) { commit = s.substring("commit".length()); break; } } break; } return commit.trim(); } /** * Parses SCM output and returns branch of SCM. * * @param scm SCM in use for this build * @return String branch of SCM */ private String getSCMBranch(SCM scm) { String branch = "Unknown"; switch (scm) { case SVN: for (String s : scmOut) { if (s.startsWith("URL:")) { branch = s.substring(4).trim(); branch = getSvnUriInfo(branch)[1]; break; } } break; case GIT: for (String s : scmOut) { if (s.startsWith("*")) { branch = s.substring("*".length()); break; } } break; } return branch.trim(); } /** * Reads and returns the full contents of the specified file. * * @param file File to read * @return byte[] containing full contents of file * @throws IOException if there is an I/O error while reading the file */ private byte[] readFile(File file) throws IOException { RandomAccessFile raf = new RandomAccessFile(file, "r"); byte[] buffer = new byte[(int) raf.length()]; raf.readFully(buffer); raf.close(); return buffer; } /** * Given a list of files, computes and returns an MD5 checksum of the full * contents of all files. * * @param files List<File> containing every file to input into the MD5 checksum * @return byte[] calculated MD5 checksum * @throws IOException if there is an I/O error while reading a file * @throws NoSuchAlgorithmException if the MD5 algorithm is not supported */ private byte[] computeMD5(List<File> files) throws IOException, NoSuchAlgorithmException { MessageDigest md5 = MessageDigest.getInstance("MD5"); for (File file : files) { getLog().debug("Computing MD5 for: " + file); md5.update(readFile(file)); } return md5.digest(); } /** * Converts bytes to a hexadecimal string representation and returns it. * * @param array byte[] to convert * @return String containing hexadecimal representation of bytes */ private String byteArrayToString(byte[] array) { StringBuilder sb = new StringBuilder(); for (byte b : array) { sb.append(Integer.toHexString(0xff & b)); } return sb.toString(); } /** * Computes and returns an MD5 checksum of the contents of all files in the * input Maven FileSet. * * @return String containing hexadecimal representation of MD5 checksum * @throws Exception if there is any error while computing the MD5 checksum */ private String computeMD5() throws Exception { List<File> files = FileSetUtils.convertFileSetToFiles(source); // File order of MD5 calculation is significant. Sorting is done on // unix-format names, case-folded, in order to get a platform-independent // sort and calculate the same MD5 on all platforms. Collections.sort(files, new Comparator<File>() { @Override public int compare(File lhs, File rhs) { return normalizePath(lhs).compareTo(normalizePath(rhs)); } private String normalizePath(File file) { return file.getPath().toUpperCase().replaceAll("\\\\", "/"); } }); byte[] md5 = computeMD5(files); String md5str = byteArrayToString(md5); getLog().info("Computed MD5: " + md5str); return md5str; } }