The BuildManager class

When developing industrial-strength software, the management of build numbers is a key issue. If you make sure that every binary release is tagged with its unique build number, you can significantly reduce debugging time, since you (the devloper) know the exact version where the bug occurs. Additionally, you can also easily determine whether a given executable can support a specific feature, just by looking at the build number reported by that executable.
Ideally, build numbers should be automatically managed, thereby eliminating those (silly) human errors: "Oops, I forgot to update the build number on that exeuctable which we released yesterday and has already been downloaded 17,463 times".

This is where the BuildManager class comes into play.

This simple Java class provides two complementing services:
  • First, when invoked as a Java program (thru its main() method) it will update the build number in a properties file (specified at the command line).
  • Second, when used as a library (API) by another program, it will provide the build information through its getter methods: getBuildDate(), getBuildTime(), getBuild() and getVersion(). This information can be used by the calling code to produce the text in "About" boxes or in help messages printed to the console.
In a typical Java project you should take the following steps to have BuildManager take control over your build details:
  1. Create an empty properties file and place it somewhere on your source tree (for example: src/my/application/misc/build.props).
  2. Make sure your build script (usually: Ant's build.xml file) copies all non *.java files into the output directory.
  3. Add an action to your build script which inovkes BuildManager as a java program. The path to the properties file, src/my/application/misc/build.props, should be specified as the command line argument of this invocation.
  4. In your program, when you need to read the build number, insert this code fragment: new BuildManager(BuildManager.class.getResourceAsStream("/src/my/application/misc/build.props")).getBuild()

import java.io.*;
import java.net.URL;
import java.text.*;
import java.util.*;

public class BuildManager
{
   private static final String BUILD_ID = "build-number";
   private static final String WHEN = "build-date";
   private static final String VERSION = "version-number";
   
   private static final Locale LOCALE = Locale.UK;
   
   private final Properties p = new Properties();
   private final File f;
   private int build = 126;
   private Date when;
   private String version = "1.2";
   
   private DateFormat df = DateFormat.getDateTimeInstance(DateFormat.LONG, 
      DateFormat.LONG, LOCALE);
   
   public BuildManager(URL url) throws Exception 
   {
      f = new File(url.getFile());
      if(f.exists())
      {
         InputStream is = new FileInputStream(f);
         init(is);
      }         
   }

   public BuildManager(InputStream is) throws Exception
   {
      f = null;
      init(is);
   }
   
   private void init(InputStream is) throws IOException
   {
      try 
      {
         load(is);
      }
      finally
      {
         if(is != null)
            is.close();
      }
   }
   
   private void load(InputStream is) throws IOException
   {     
      p.load(is);
      try
      {
         Integer n = Integer.valueOf(p.getProperty(BUILD_ID));
         if(n != null)
            build = n.intValue();
      }
      catch(Throwable e)
      {
         // Absorb
      }
      
      try
      {
         String d = p.getProperty(WHEN);
         if(d != null)
            when = df.parse(d);
      }
      catch(Throwable e)
      {
         // Absorb
      }   
      
      try
      {
         String ver = p.getProperty(VERSION);
         if(ver != null)
            version = ver;
      }
      catch(Throwable e)
      {
         // Absorb
      }   
   }
   
   private void step(String newVersion) throws Exception
   {
      if(f == null)
         throw new Exception("I Cannot invoked step() if the " +
                "BuildManager(InputStream) constrcutor was used");
      
      if(newVersion != null)
      {
         if(!version.equals(newVersion)) 
         {
            version = newVersion;
            build = 0;
         }
      }
      
      p.setProperty(VERSION, version);
      
      build += 1;
      p.setProperty(BUILD_ID, Integer.toString(build));

      Calendar cal = newCalendar();
      when = cal.getTime();
      df.setCalendar(cal);
      String temp = df.format(when);      
      p.setProperty(WHEN, temp);
      
      
      OutputStream os = new FileOutputStream(f);
      try
      {
         p.store(os, "build-manager");
      }
      finally
      {
         if(os != null)
            os.close();
      }
   }
   
   private static Calendar newCalendar()
   {
      Calendar cal = Calendar.getInstance(LOCALE);
      cal.setTimeZone(TimeZone.getTimeZone("GMT"));
      
      return cal;      
   }
   
   public String getBuildDate()
   {
      Calendar cal = newCalendar();
      cal.setTime(when);
      
      DateFormat ddff = new SimpleDateFormat("yyyy.MM.dd");
      ddff.setCalendar(cal);
      
      return ddff.format(cal.getTime());         
   }
   
   public String getBuildTime()
   {
      Calendar cal = newCalendar();
      cal.setTime(when);
      
      DateFormat ddff = new SimpleDateFormat("HH.mm");
      ddff.setCalendar(cal);
      
      return ddff.format(cal.getTime());         
   }
   
   public int getBuild() 
   {
      return build;
   }
   
   public String getVersion()
   {
      return version;
   }
   
   private static void usage()
   {
      System.err.println("BuildManager: A Build-tracking utility");
      System.err.println("Usage: BuildManager <file-name> [<version>]");
      System.err.println("    <file-name> Name of a properties file " +
            "maintaining build information");
      System.err.println("    <version> An optional version string");      
      System.err.println("This program updates the " + BUILD_ID + ", " 
         + VERSION + " and " + WHEN + " entries at the specified file");
      System.err.println("The time zone used is GMT");
      System.err.println();
      System.exit(-1);
   }
   
   public static void main(String[] args)
   {
      if(args.length < 1 || args.length > 2)
         usage();
      
      for(int i = 0; i < args.length; ++i)
         if(args[i].startsWith("-"))
            usage();
      
      String ver = null;
      if(args.length == 2)
         ver = args[1];
      
      try
      {
         URL url = new File(args[0]).getAbsoluteFile().toURL();
         BuildManager bm = new BuildManager(url);         
         bm.step(ver);
         
         System.exit(0);
      }
      catch(Throwable t)
      {
         System.err.println("Failure: " + t.getMessage());
         System.exit(-2);
      }
   }
}