The values come from the manifest of that artifact, falling back to its Maven descriptor when + * Flume has been shaded into another artifact and the manifest is no longer its own. Every accessor + * returns {@value #UNKNOWN} rather than {@code null} when the information is unavailable. */ - public class VersionInfo { - private static Package myPackage; - private static VersionAnnotation version; + private static final String UNKNOWN = "Unknown"; + + private static final String GROUP_ID = "org.apache.flume"; + private static final String ARTIFACT_ID = "flume-ng-core"; + + private static final String IMPLEMENTATION_TIMESTAMP = "Implementation-Timestamp"; + private static final String PURL = "Purl"; + private static final String BUNDLE_SCM = "Bundle-SCM"; + + private static final String PURL_PREFIX = "pkg:maven/" + GROUP_ID + "/" + ARTIFACT_ID + "@"; + private static final String CLASS_PATH = "org/apache/flume/tools/VersionInfo.class"; + private static final String MANIFEST_PATH = "META-INF/MANIFEST.MF"; + private static final String POM_PROPERTIES_PATH = + "/META-INF/maven/" + GROUP_ID + "/" + ARTIFACT_ID + "/pom.properties"; + + /** Matches one {@code name=value} pair of an OSGi header, with an optionally quoted value. */ + private static final Pattern SCM_ATTRIBUTE = + Pattern.compile("(?:^|,)\\s*([A-Za-z0-9_-]+)\\s*=\\s*(?:\"([^\"]*)\"|([^,]*))"); + + private static final Attributes MANIFEST = ownManifest(); + private static final Properties POM_PROPERTIES = pomProperties(); + + private static final String VERSION = version(MANIFEST, POM_PROPERTIES); + private static final String PURL_VALUE = purl(MANIFEST, POM_PROPERTIES); + private static final String URL_VALUE = orUnknown(scmAttribute(MANIFEST.getValue(BUNDLE_SCM), "url")); + private static final String TAG = orUnknown(scmAttribute(MANIFEST.getValue(BUNDLE_SCM), "tag")); + private static final String DATE = orUnknown(MANIFEST.getValue(IMPLEMENTATION_TIMESTAMP)); + + /** + * Reads the manifest of the artifact this class was loaded from. + * + *
Resolving it against the location of this class, instead of looking up + * {@code META-INF/MANIFEST.MF} on the class path, keeps another artifact from answering. Returns + * empty attributes when the manifest is missing or belongs to an artifact Flume was shaded into. + */ + private static Attributes ownManifest() { + URL self = VersionInfo.class.getResource("VersionInfo.class"); + if (self == null) { + return new Attributes(); + } + String location = self.toString(); + if (!location.endsWith(CLASS_PATH)) { + return new Attributes(); + } + String root = location.substring(0, location.length() - CLASS_PATH.length()); + try (InputStream stream = URI.create(root + MANIFEST_PATH).toURL().openStream()) { + Attributes attributes = new Manifest(stream).getMainAttributes(); + return isOwn(attributes) ? attributes : new Attributes(); + } catch (IOException | RuntimeException ignored) { + return new Attributes(); + } + } + + private static Properties pomProperties() { + Properties properties = new Properties(); + try (InputStream stream = VersionInfo.class.getResourceAsStream(POM_PROPERTIES_PATH)) { + if (stream != null) { + properties.load(stream); + } + } catch (IOException | RuntimeException ignored) { + // Falls through to the empty properties. + } + return properties; + } + + /** Tells whether the manifest describes this artifact rather than one Flume was shaded into. */ + static boolean isOwn(Attributes manifest) { + String purl = manifest.getValue(PURL); + return purl != null && purl.startsWith(PURL_PREFIX); + } - static { - myPackage = VersionAnnotation.class.getPackage(); - version = myPackage.getAnnotation(VersionAnnotation.class); + static String version(Attributes manifest, Properties pomProperties) { + String version = manifest.getValue(Attributes.Name.IMPLEMENTATION_VERSION); + return orUnknown(version != null ? version : pomProperties.getProperty("version")); + } + + static String purl(Attributes manifest, Properties pomProperties) { + String purl = manifest.getValue(PURL); + if (purl == null) { + String groupId = pomProperties.getProperty("groupId"); + String artifactId = pomProperties.getProperty("artifactId"); + String version = pomProperties.getProperty("version"); + if (groupId != null && artifactId != null && version != null) { + purl = "pkg:maven/" + groupId + "/" + artifactId + "@" + version; + } + } + return orUnknown(purl); } /** - * Get the meta-data for the Flume package. - * @return + * Returns one attribute of an OSGi {@code Bundle-SCM} header, or {@code null} if absent. + * + *
The header is specified by OSGi Core R8, section 3.2.1, as a comma separated list of + * {@code url}, {@code connection}, {@code developer-connection} and {@code tag} attributes. */ - static Package getPackage() { - return myPackage; + static String scmAttribute(String header, String attribute) { + if (header == null) { + return null; + } + Matcher matcher = SCM_ATTRIBUTE.matcher(header); + while (matcher.find()) { + if (attribute.equals(matcher.group(1))) { + String quoted = matcher.group(2); + return quoted != null ? quoted : matcher.group(3).trim(); + } + } + return null; + } + + private static String orUnknown(String value) { + return value != null && !value.isEmpty() ? value : UNKNOWN; } /** - * Get the Flume version. - * @return the Flume version string, eg. "1.1" + * Gets the Flume version. + * + * @return the Flume version string, eg. "2.0.0" */ public static String getVersion() { - return version != null ? version.version() : "Unknown"; + return VERSION; + } + + /** + * Gets the Package URL of the Flume artifact this class was loaded from. + * + * @return the Package URL, eg. "pkg:maven/org.apache.flume/flume-ng-core@2.0.0" + */ + public static String getPurl() { + return PURL_VALUE; } /** - * Get the subversion revision number for the root directory - * @return the revision number, eg. "100755" + * Gets the source control revision this was built from. + * + *
The build no longer records a commit id, since it has to produce the same artifact from a + * Git checkout and from the source distribution, which carries no repository metadata. + * + * @return always "Unknown" */ public static String getRevision() { - if (version != null && version.revision() != null && !version.revision().isEmpty()) { - return version.revision(); - } - return "Unknown"; + return UNKNOWN; } /** - * Get the branch on which this originated. - * @return The branch name, e.g. "trunk" or "branches/branch-1.1" + * Gets the source control tag or branch this was built from. + * + * @return the tag, eg. "rel/2.0.0" */ public static String getBranch() { - return version != null ? version.branch() : "Unknown"; + return TAG; } /** - * The date that Flume was compiled. - * @return the compilation date in unix date format + * Gets the date Flume was built. + * + * @return the build date in ISO-8601 format */ public static String getDate() { - return version != null ? version.date() : "Unknown"; + return DATE; } /** - * The user that compiled Flume. - * @return the username of the user + * Gets the user that compiled Flume. + * + * @return always "Unknown" + * @deprecated Recording the user would make the build unreproducible. */ + @Deprecated(since = "2.0.0", forRemoval = true) public static String getUser() { - return version != null ? version.user() : "Unknown"; + return UNKNOWN; } /** - * Get the subversion URL for the root Flume directory. + * Gets the source control URL of the Flume repository. + * + * @return the repository URL */ public static String getUrl() { - return version != null ? version.url() : "Unknown"; + return URL_VALUE; } /** - * Get the checksum of the source files from which Flume was - * built. - **/ + * Gets the checksum of the source files Flume was built from. + * + * @return always "Unknown" + * @deprecated Use {@link #getPurl()} to identify the artifact, and verify it against the + * checksums published with the release. + */ + @Deprecated(since = "2.0.0", forRemoval = true) public static String getSrcChecksum() { - return version != null ? version.srcChecksum() : "Unknown"; + return UNKNOWN; } - /** - * Returns the build version info which includes version, - * revision, user, date and source checksum - */ + /** Returns the build version info, which includes the version, the tag and the build date. */ public static String getBuildVersion() { - return VersionInfo.getVersion() + " from " - + VersionInfo.getRevision() + " by " - + VersionInfo.getUser() + " on " - + VersionInfo.getDate() + " source checksum " - + VersionInfo.getSrcChecksum(); + return getVersion() + " from " + getBranch() + " built on " + getDate(); } public static void main(String[] args) { System.out.println("Flume " + getVersion()); - System.out.println("Source code repository: " + "https://git.apache.org/repos/asf/flume.git"); - System.out.println("Revision: " + getRevision()); - System.out.println("Compiled by " + getUser() + " on " + getDate()); - System.out.println("From source with checksum " + getSrcChecksum()); + System.out.println("Package URL: " + getPurl()); + System.out.println("Source code repository: " + getUrl()); + System.out.println("Tag: " + getBranch()); + System.out.println("Compiled on " + getDate()); } } diff --git a/flume-ng-core/src/test/java/org/apache/flume/tools/TestVersionInfo.java b/flume-ng-core/src/test/java/org/apache/flume/tools/TestVersionInfo.java index 59457f96e8..66d70734f7 100644 --- a/flume-ng-core/src/test/java/org/apache/flume/tools/TestVersionInfo.java +++ b/flume-ng-core/src/test/java/org/apache/flume/tools/TestVersionInfo.java @@ -16,42 +16,134 @@ */ package org.apache.flume.tools; -import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import java.time.Instant; +import java.util.Properties; +import java.util.jar.Attributes; import org.junit.Test; public class TestVersionInfo { - private static final Logger logger = LogManager.getLogger(); + private static final String PURL = "pkg:maven/org.apache.flume/flume-ng-core@2.0.0"; + + private static final String BUNDLE_SCM = "url=\"https://gitbox.apache.org/repos/asf/logging-flume.git\"," + + "connection=\"scm:git:https://gitbox.apache.org/repos/asf/logging-flume.git\"," + + "developer-connection=\"scm:git:https://gitbox.apache.org/repos/asf/logging-flume.git\"," + + "tag=\"rel/2.0.0\""; /** - * Make sure that Unknown is expected when no version info + * Checks the metadata of the artifact the test runs against. + * + *
BND writes the manifest to the output directory before the tests run, so the values are
+ * available whether Flume is loaded from a JAR or from the compiled classes.
*/
@Test
- public void testVersionInfoUnknown() {
+ public void testMetadataOfOwnArtifact() {
+ assertTrue(
+ "getVersion returned " + VersionInfo.getVersion(),
+ VersionInfo.getVersion().matches("\\d+\\.\\d+.*"));
+ assertTrue(
+ "getPurl returned " + VersionInfo.getPurl(),
+ VersionInfo.getPurl().startsWith("pkg:maven/org.apache.flume/flume-ng-core@"));
+ assertTrue(
+ "getUrl returned " + VersionInfo.getUrl(),
+ VersionInfo.getUrl().startsWith("https://")
+ && VersionInfo.getUrl().contains("logging-flume"));
+ assertTrue(
+ "getBranch returned " + VersionInfo.getBranch(),
+ VersionInfo.getBranch().startsWith("rel/"));
+ // Throws if the timestamp is not ISO-8601.
+ Instant.parse(VersionInfo.getDate());
+ assertTrue(
+ "getBuildVersion returned " + VersionInfo.getBuildVersion(),
+ VersionInfo.getBuildVersion().matches(".+ from .+ built on .+"));
+ }
- logger.debug("Flume " + VersionInfo.getVersion());
- logger.debug("Subversion " + VersionInfo.getUrl() + " -r " + VersionInfo.getRevision());
- logger.debug("Compiled by " + VersionInfo.getUser() + " on " + VersionInfo.getDate());
- logger.debug("From source with checksum " + VersionInfo.getSrcChecksum());
- logger.debug("Flume " + VersionInfo.getBuildVersion());
+ @Test
+ @SuppressWarnings({"deprecation", "removal"})
+ public void testUnrecordedMetadata() {
+ assertEquals("Unknown", VersionInfo.getRevision());
+ assertEquals("Unknown", VersionInfo.getUser());
+ assertEquals("Unknown", VersionInfo.getSrcChecksum());
+ }
- assertTrue("getVersion returned Unknown", !VersionInfo.getVersion().equals("Unknown"));
- assertTrue("getUser returned Unknown", !VersionInfo.getUser().equals("Unknown"));
- assertTrue("getUrl returned Unknown", !VersionInfo.getUrl().equals("Unknown"));
- assertTrue(
- "getSrcChecksum returned Unknown", !VersionInfo.getSrcChecksum().equals("Unknown"));
+ @Test
+ public void testVersionPrefersTheManifest() {
+ Attributes manifest = new Attributes();
+ manifest.putValue("Implementation-Version", "2.0.0");
+ assertEquals("2.0.0", VersionInfo.version(manifest, pomProperties("1.11.0")));
+ }
- // check getBuildVersion() return format
- assertTrue(
- "getBuildVersion returned unexpected format",
- VersionInfo.getBuildVersion().matches(".+from.+by.+on.+source checksum.+"));
+ @Test
+ public void testVersionFallsBackToPomProperties() {
+ assertEquals("1.11.0", VersionInfo.version(new Attributes(), pomProperties("1.11.0")));
+ }
+
+ @Test
+ public void testVersionWithoutAnySource() {
+ assertEquals("Unknown", VersionInfo.version(new Attributes(), new Properties()));
+ }
+
+ @Test
+ public void testPurlPrefersTheManifest() {
+ Attributes manifest = new Attributes();
+ manifest.putValue("Purl", PURL);
+ assertEquals(PURL, VersionInfo.purl(manifest, pomProperties("1.11.0")));
+ }
+
+ @Test
+ public void testPurlIsBuiltFromPomProperties() {
+ assertEquals(
+ "pkg:maven/org.apache.flume/flume-ng-core@1.11.0",
+ VersionInfo.purl(new Attributes(), pomProperties("1.11.0")));
+ }
+
+ @Test
+ public void testPurlWithoutAnySource() {
+ assertEquals("Unknown", VersionInfo.purl(new Attributes(), new Properties()));
+ }
+
+ /** A manifest of an artifact Flume was shaded into must not be mistaken for our own. */
+ @Test
+ public void testForeignManifestIsRejected() {
+ Attributes foreign = new Attributes();
+ foreign.putValue("Purl", "pkg:maven/com.example/uber-jar@1.0.0");
+ foreign.putValue("Implementation-Version", "1.0.0");
+ assertFalse(VersionInfo.isOwn(foreign));
+ assertFalse(VersionInfo.isOwn(new Attributes()));
+
+ Attributes own = new Attributes();
+ own.putValue("Purl", PURL);
+ assertTrue(VersionInfo.isOwn(own));
+ }
+
+ @Test
+ public void testScmAttributes() {
+ assertEquals("rel/2.0.0", VersionInfo.scmAttribute(BUNDLE_SCM, "tag"));
+ assertEquals(
+ "https://gitbox.apache.org/repos/asf/logging-flume.git", VersionInfo.scmAttribute(BUNDLE_SCM, "url"));
+ assertEquals(
+ "scm:git:https://gitbox.apache.org/repos/asf/logging-flume.git",
+ VersionInfo.scmAttribute(BUNDLE_SCM, "developer-connection"));
+ assertNull(VersionInfo.scmAttribute(BUNDLE_SCM, "revision"));
+ assertNull(VersionInfo.scmAttribute(null, "tag"));
+ }
+
+ /** OSGi only requires quoting for values with special characters. */
+ @Test
+ public void testScmAttributeWithoutQuotes() {
+ assertEquals("rel/2.0.0", VersionInfo.scmAttribute("url=https://example.org,tag=rel/2.0.0", "tag"));
+ }
- // "Unknown" when build without svn or git
- assertNotNull("getRevision returned null", VersionInfo.getRevision());
- assertNotNull("getBranch returned null", VersionInfo.getBranch());
+ private static Properties pomProperties(String version) {
+ Properties properties = new Properties();
+ properties.setProperty("groupId", "org.apache.flume");
+ properties.setProperty("artifactId", "flume-ng-core");
+ properties.setProperty("version", version);
+ return properties;
}
}
diff --git a/flume-ng-dist/pom.xml b/flume-ng-dist/pom.xml
index 6e92bc1d6c..2faad49d51 100644
--- a/flume-ng-dist/pom.xml
+++ b/flume-ng-dist/pom.xml
@@ -21,7 +21,7 @@