How to automate inclusion of versioning info in Java beans

This post is about how to solve a problem that bugged me for while. 

When developing Portlets for Liferay one always wonders what exact version is actually running on the various servers of the development chain (local/test/integration/production) and what sources it was compiled from.

  • The first solution (when still using CVS/SVN):

When using CVS or SVN this can easily be solved by using keyword expansion and have either one of these systems update the MANIFEST.MF file containing the keyword placeholders.

For SVN adding keywords to the MANIFEST.MF file and simply adding a couple of lines to the build.xml file that change the MANIFEST.MF file (so it gets marked for check-in every time), the requested information can be included pretty automatically.

   1: <?xml version="1.0"?>

   2: <!DOCTYPE project>

   3:  

   4: <project name="my-portlet" basedir="." default="deploy">

   5:     <import file="../build-common-portlet.xml" />

   6:     <tstamp>

   7:         <format property="TimeDate.Now" pattern="yyyy-MM-dd HH:mm:ss" />

   8:     </tstamp>

   9:     <manifest file="docroot/META-INF/MANIFEST.MF" mode="update">

  10:         <attribute name="Ant-Build-Stamp" value="${TimeDate.Now}" />

  11:     </manifest>

  12: </project>

In the MANIFEST.MF file one needs to include the following placeholders:

   1: Svn-Revision: $Revision$

   2: Svn-Author: $Author$

   3: Svn-Date: $Date$

Finally in Eclipse one needs to add the keywords to the MANIFEST.MF file (this feature is hidden in the MANIFEST.MF context menu under TEAM|Set Property…. In the resulting dialog choose ‘svn:keywords’ and enter ‘Revision Date Author‘ on a single line.

CVS always expands the keywords if present so only the Ant script is needed.

Getting hold of this information and making use of it is similar to the Mercurial solution presented next.

  • The second solution (after switching to Mercurial):

This method adds Mercurial information to the MANIFEST.MF unfortunately this is immediately outdated the moment one does a commit/push (because then a new revision is created).

BUT when the portlet is build for deployment the information in the MANIFEST.MF is updated to the correct values prior to compiling. The deployment build itself does not commit any files so no new revision is created. So when deployed one can easily find out what exact source code is used and compiled.

The first part is to prep the build.xml file so it retrieves and writes/updates this information into the MANIFEST/MF file.

A plain Liferay generated build.xml file looks like:

   1: <?xml version="1.0"?>

   2: <project name="my-portlet" basedir="." default="deploy">

   3:     <import file="../build-common-portlet.xml" />

   4:  

   5: </project>

By inserting/include the following xml just after the import task we retrieve the wanted information and write/update it into the MANIFEST.MF file. This file and the docroot/META-INF directory where it should reside are also created if missing.

   1: <import file="manifest.xml" />

 

The manifest.xml should contain the following Ant script:

   1: <?xml version="1.0"?>

   2: <project>

   3:     <if>

   4:  

   5:         <!-- Execute ony once, saves time -->

   6:  

   7:         <not>

   8:             <isset property="hgrevision" />

   9:         </not>

  10:  

  11:         <then>

  12:  

  13:             <!-- Test Operating Systems -->

  14:  

  15:             <condition property="isUnix">

  16:                 <os family="unix" />

  17:             </condition>

  18:  

  19:             <condition property="isWindows">

  20:                 <os family="windows" />

  21:             </condition>

  22:  

  23:             <!-- Set Mercurial Executable -->

  24:  

  25:             <if>

  26:                 <isset property="isUnix" />

  27:                 <then>

  28:                     <property name="mercurial" value="hg" />

  29:                 </then>

  30:             </if>

  31:  

  32:             <if>

  33:                 <isset property="isWindows" />

  34:                 <then>

  35:                     <property name="mercurial" value="${env.ProgramFiles}/Mercurial/hg.exe" />

  36:                 </then>

  37:             </if>

  38:  

  39:             <echo message="Using: ${mercurial}" />

  40:  

  41:             <!-- Run Mercurial for Information -->

  42:  

  43:             <exec executable="${mercurial}">

  44:                 <arg value="id" />

  45:                 <arg value="-n" />

  46:                 <redirector outputproperty="hgrevision" />

  47:             </exec>

  48:             

  49:             <!-- Trim any trailing + sign -->

  50:  

  51:             <script language="javascript">
   1:  

   2:                 var hgrevision =

   3:                 project.getProperty("hgrevision");

   4:                 project.setProperty("hgrevision",

   5:                 hgrevision.replaceAll("[\+]", ""));

   6:             

</script>

  52:             <echo message="Local Revision: ${hgrevision}" />

  53:  

  54:             <exec executable="${mercurial}">

  55:                 <arg value="id" />

  56:                 <arg value="-t" />

  57:                 <redirector outputproperty="hgtags" />

  58:             </exec>

  59:             <echo message="Tag: ${hgtags}" />

  60:  

  61:             <exec executable="${mercurial}">

  62:                 <arg value="id" />

  63:                 <arg value="-b" />

  64:                 <redirector outputproperty="hgbranch" />

  65:             </exec>

  66:             <echo message="Branch: ${hgbranch}" />

  67:  

  68:             <exec executable="${mercurial}">

  69:                 <arg value="log" />

  70:                 <arg value="-r${hgrevision}" />

  71:                 <arg value="--template" />

  72:                 <arg value='&quot;{date|isodate}&quot;' />

  73:                 <redirector outputproperty="hgdate" />

  74:             </exec>

  75:             <echo message="Date: ${hgdate}" />

  76:  

  77:             <exec executable="${mercurial}">

  78:                 <arg value="log" />

  79:                 <arg value="-r${hgrevision}" />

  80:                 <arg value="--template" />

  81:                 <arg value='&quot;{node}&quot;' />

  82:                 <redirector outputproperty="hgnode" />

  83:             </exec>

  84:             <echo message="Node: ${hgnode}" />

  85:  

  86:             <exec executable="${mercurial}">

  87:                 <arg value="log" />

  88:                 <arg value="-r${hgrevision}" />

  89:                 <arg value="--template" />

  90:                 <arg value='&quot;{node|short}&quot;' />

  91:                 <redirector outputproperty="hgsnode" />

  92:             </exec>

  93:             <echo message="Short Node: ${hgsnode}" />

  94:  

  95:             <exec executable="${mercurial}">

  96:                 <arg value="log" />

  97:                 <arg value="-r${hgrevision}" />

  98:                 <arg value="--template" />

  99:                 <arg value='&quot;{author}&quot;' />

 100:                 <redirector outputproperty="hgauthor" />

 101:             </exec>

 102:             <echo message="Author: ${hgauthor}" />

 103:  

 104:             <exec executable="${mercurial}">

 105:                 <arg value="log" />

 106:                 <arg value="-r${hgrevision}" />

 107:                 <arg value="--template" />

 108:                 <arg value='&quot;{repo}&quot;' />

 109:                 <redirector outputproperty="hgrepo" />

 110:             </exec>

 111:             <echo message="Repository: ${hgrepo}" />

 112:  

 113:             <!-- Ant Built-in properties -->

 114:  

 115:             <echo message="Java Version: ${java.runtime.version}" />

 116:             <echo message="Java Vendor: ${java.vendor}" />

 117:  

 118:             <!-- Liferay properties (see build.properties of SDK) -->

 119:  

 120:             <echo message="Liferay-Version: ${lp.version}" />

 121:  

 122:             <!-- Ant Build timestamp -->

 123:  

 124:             <tstamp>

 125:                 <format property="TimeDate.Now" pattern="yyyy-MM-dd HH:mm:ss" />

 126:             </tstamp>

 127:  

 128:             <!-- Create META-INF if missing -->

 129:  

 130:             <if>

 131:                 <not>

 132:                     <available file="docroot/META-INF" type="dir" />

 133:                 </not>

 134:                 <then>

 135:                     <mkdir dir="docroot/META-INF" />

 136:                 </then>

 137:             </if>

 138:  

 139:             <manifest file="docroot/META-INF/MANIFEST.MF" mode="update">

 140:  

 141:                 <!-- Mercurial Information -->

 142:  

 143:                 <attribute name="Hg-Revision" value="${hgrevision}" />

 144:                 <attribute name="Hg-Tags" value="${hgtags}" />

 145:                 <attribute name="Hg-Branch" value="${hgbranch}" />

 146:                 <attribute name="Hg-Date" value="${hgdate}" />

 147:                 <attribute name="Hg-Node" value="${hgnode}" />

 148:                 <attribute name="Hg-Short-Node" value="${hgsnode}" />

 149:                 <attribute name="Hg-Author" value="${hgauthor}" />

 150:                 <attribute name="Hg-Repository" value="${hgrepo}" />

 151:  

 152:                 <!-- Ant Built-in properties -->

 153:  

 154:                 <attribute name="Java-Version" value="${java.runtime.version}" />

 155:                 <attribute name="Java-Vendor" value="${java.vendor}" />

 156:  

 157:                 <!-- Liferay properties (see build.properties of SDK) -->

 158:  

 159:                 <attribute name="Liferay-Version" value="${lp.version}" />

 160:  

 161:                 <attribute name="Ant-Build-Stamp" value="${TimeDate.Now}" />

 162:             </manifest>

 163:         </then>

 164:     </if>

 165: </project>

Notes:

  • The above script expects hg Mercurial executable to be on the path under Linux and to be installed in “c:\program files\Mercurial” when running under Windows.
  • The Short-Node consists of the first 12 characters from the full Mercurial ”’Node”’ hash. This corresponds to the change set value that Source forge displays when browsing the repository.
  • The Revision is the local revision within a project repository (in contrast to the system wide values of node and short-node). It corresponds to the change set number that Source forge displays when browsing the repository.
  • Because of the small piece of JavaScript to cleanup the revision number, Java 1.6 or later is required.

The resulting MANIFEST.MF looks like:

   1: Manifest-Version: 1.0

   2: Ant-Version: Apache Ant 1.7.1

   3: Created-By: 20.4-b02 (Sun Microsystems Inc.)

   4: Hg-Revision: 27

   5: Hg-Tags: tip

   6: Hg-Branch: my-portlet

   7: Hg-Date: 2012-08-14 16:41 +0200

   8: Hg-Node: cdd2c1242f3e3f528ff1e71022570d57d1b9a342

   9: Hg-Short-Node: cdd2c1242f3e

  10: Hg-Author: me

  11: Hg-Repository: 9876543219101112131415161718192021222324252621

  12: Java-Version: 1.6.0_33-b03

  13: Liferay-Version: 6.0.12

  14: Ant-Build-Stamp: 2012-08-15 12:17:12

  15: Java-Vendor: Sun Microsystems Inc.

  16: Class-Path: 

The lines between Created-By: and Class-Path: are originating from added the Ant build.xml script.

  • Finally how to retrieve this information in out Java code:

We need to add some utility methods to our code:

   1: /**

   2:  * Retrieves a property from /META-INF/MANIFEST.MF without a prefix.

   3:  * 

   4:  * @param property

   5:  *            the property to retrieve

   6:  * @return the property value, 'na' or 'error'.

   7:  */

   8: public static String getManifestProperty(final String property) {

   9:     if (_log == null) {

  10:         _log = LogFactoryUtil.getLog(MyPortlet.class);

  11:     }

  12:  

  13:     Properties prop = new Properties();

  14:  

  15:     try {

  16:         prop.load(FacesContext.getCurrentInstance().getExternalContext()

  17:                 .getResourceAsStream("/" + JarFile.MANIFEST_NAME));

  18:  

  19:         String rev = prop.getProperty(property);

  20:  

  21:         return rev == null ? "na" : rev.trim();

  22:     } catch (IOException e) {

  23:         _log.error("Error retrieving " + property + " Property from /"

  24:                 + JarFile.MANIFEST_NAME + " (" + e.getMessage() + ").");

  25:     }

  26:  

  27:     return "error";

  28: }

  29:  

  30: /**

  31:  * Retrieves a property from /META-INF/MANIFEST.MF with an optional prefix.

  32:  * 

  33:  * @param prefix

  34:  *            a prefix like 'Svn-' or 'Hg-' in our case.

  35:  * @param property

  36:  *            the property to retrieve

  37:  * @return the property value, 'na' or 'error'.

  38:  */

  39: public static String getManifestProperty(final String prefix,

  40:         final String property) {

  41:     return getManifestProperty(prefix + property);

  42: }

  43:  

  44: /**

  45:  * Retrieves a 'Hg-' prefixed property from /META-INF/MANIFEST.MF and

  46:  * cleans the $Keyword$ definition from the result.

  47:  * 

  48:  * @param property

  49:  *            the property to retrieve

  50:  * @return the property value, 'na' or 'error'.

  51:  */

  52: public static String getManifestHgProperty(final String property) {

  53:     return getManifestProperty("Hg-", property)

  54:             .replace("$" + property + ": ", "").replace(" $", "").trim();

  55: }

  56:  

  57: public static String LPad(final String str, final int length) {

  58:     return LPad(str, length, ' ');

  59: }

  60:  

  61: public static String LPad(final String str, final int length, final char car) {

  62:     return str

  63:             + String.format("%" + (length - str.length()) + "s", "")

  64:                     .replace(" ", String.valueOf(car));

  65: }

  66:  

  67: public static String RPad(final String str, final int length) {

  68:     return RPad(str, length, ' ');

  69: }

  70:  

  71: public static String RPad(final String str, final int length, final char car) {

  72:     return String.format("%" + (length - str.length()) + "s", "").replace(

  73:             " ", String.valueOf(car))

  74:             + str;

  75: }

and call use code for example in the constructor of our Java backing bean like:

   1: _log.info("------------------------------------");

   2:  

   3: _log.info(GroupwallHelpers.LPad("Class:", 32) + getClass().getName());

   4:  

   5: _log.info(GroupwallHelpers.LPad("Revision:", 32)

   6:         + GroupwallHelpers.getManifestHgProperty("Revision"));

   7: _log.info(GroupwallHelpers.LPad("Node:", 32)

   8:         + GroupwallHelpers.getManifestHgProperty("Node"));

   9: _log.info(GroupwallHelpers.LPad("Short-Node:", 32)

  10:         + GroupwallHelpers.getManifestHgProperty("Short-Node"));

  11: _log.info(GroupwallHelpers.LPad("Tags:", 32)

  12:         + GroupwallHelpers.getManifestHgProperty("Tags"));

  13: _log.info(GroupwallHelpers.LPad("Branch:", 32)

  14:         + GroupwallHelpers.getManifestHgProperty("Branch"));

  15: _log.info(GroupwallHelpers.LPad("Repository:", 32)

  16:         + GroupwallHelpers.getManifestHgProperty("Repository"));

  17: _log.info(GroupwallHelpers.LPad("Date:", 32)

  18:         + GroupwallHelpers.getManifestHgProperty("Date"));

  19: _log.info(GroupwallHelpers.LPad("Author:", 32)

  20:         + GroupwallHelpers.getManifestHgProperty("Author"));

  21:  

  22: _log.info(GroupwallHelpers.LPad("Ant-Build: ", 32)

  23:         + GroupwallHelpers.getManifestProperty("Ant-Build-Stamp"));

  24:  

  25: _log.info(GroupwallHelpers.LPad("Java-Version: ", 32)

  26:         + GroupwallHelpers.getManifestProperty("Java-Version"));

  27: _log.info(GroupwallHelpers.LPad("Java-Vendor: ", 32)

  28:         + GroupwallHelpers.getManifestProperty("Java-Vendor"));

  29: _log.info(GroupwallHelpers.LPad("Ant-Build: ", 32)

  30:         + GroupwallHelpers.getManifestProperty("Liferay-Version"));

  31:  

  32: _log.info("------------------------------------");

  33:  

Final Note: A better type of output would be to use _log.debug() instead of _log.info() so the Tomcat log files are not polluted to much.

Advertisements
This entry was posted in Uncategorized. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s