views:

4148

answers:

8

I've spent far too much time trying to figure this out. This should be the simplest thing and everyone who distributes Java applications in jars must have to deal with it.

I just want to know the proper way to add versioning to my Java app so that I can access the version information when I'm testing, e.g. debugging in Eclipse and running from a jar.

Here's what I have in my build.xml:

<target name="jar" depends = "compile">
    <property name="version.num" value="1.0.0"/>
    <buildnumber file="build.num"/>
 <tstamp>
  <format property="TODAY" pattern="yyyy-MM-dd HH:mm:ss" />
 </tstamp>

    <manifest file="${build}/META-INF/MANIFEST.MF">
     <attribute name="Built-By" value="${user.name}" />
     <attribute name="Built-Date" value="${TODAY}" />          
        <attribute name="Implementation-Title" value="MyApp" />
        <attribute name="Implementation-Vendor" value="MyCompany" />       
        <attribute name="Implementation-Version" value="${version.num}-b${build.number}"/>              
    </manifest>

 <jar destfile="${build}/myapp.jar" basedir="${build}" excludes="*.jar" />       
</target>

This creates /META-INF/MANIFEST.MF and I can read the values when I'm debugging in Eclipse thusly:

public MyClass()
{
    try
    {                        
        InputStream stream = getClass().getResourceAsStream("/META-INF/MANIFEST.MF");
        Manifest manifest = new Manifest(stream);            

        Attributes attributes = manifest.getMainAttributes();

        String implementationTitle = attributes.getValue("Implementation-Title");
        String implementationVersion = attributes.getValue("Implementation-Version");
        String builtDate = attributes.getValue("Built-Date");
        String builtBy = attributes.getValue("Built-By");
   }
   catch (IOException e)
   {            
        logger.error("Couldn't read manifest.");
   }

}

But, when I create the jar file, it loads the manifest of another jar (presumably the first jar loaded by the application - in my case, activation.jar).

Also, the following code doesn't work either although all the proper values are in the manifest file.

    Package thisPackage = getClass().getPackage();
    String implementationVersion = thisPackage.getImplementationVersion();

Any ideas?

+1  A: 

Just don't use the manifest. Create a foo.properties.original file, with a content such as version=@VERSION@

And in ther same task you are jaring you can do a copy to copu foo.properties.original and then

Juan Pablo Morales
A: 

I will also usually use a version file. I will create one file per jar since each jar could have its own version.

Javamann
A: 

You can access the manifest (or any other) file within a jar if you use the same class loader to as was used to load the classes.

this.getClass().getClassLoader().getResourceAsStream( ... ) ;

If you are multi-threaded use the following:

Thread.currentThread().getContextClassLoader().getResourceAsStream( ... ) ;

This is also a realy useful technique for including a default configuration file within the jar.

Martin Spamer
+1  A: 

You want to use this:

Enumeration<URL> resources = Thread.getCurrent().getContextClassLoader().getResources("META-INF/MANIFEST.MF");

You can parse the URL to figure out WHICH jar the manifest if from and then read the URL via getInputStream() to parse the manifest.

basszero
+2  A: 

Here's what I've found that works:

packageVersion.java:

package com.company.division.project.packageversion;

import java.io.IOException;
import java.io.InputStream;
import java.util.jar.Attributes;
import java.util.jar.Manifest;

public class packageVersion
{
    void printVersion()
    {
        try
        {         
            InputStream stream = getClass().getResourceAsStream("/META-INF/MANIFEST.MF");

            if (stream == null)
            {
                System.out.println("Couldn't find manifest.");
                System.exit(0);
            }

            Manifest manifest = new Manifest(stream);

            Attributes attributes = manifest.getMainAttributes();

            String impTitle = attributes.getValue("Implementation-Title");
            String impVersion = attributes.getValue("Implementation-Version");
            String impBuildDate = attributes.getValue("Built-Date");
            String impBuiltBy = attributes.getValue("Built-By");

            if (impTitle != null)
            {
                System.out.println("Implementation-Title:   " + impTitle);
            }            
            if (impVersion != null)
            {
                System.out.println("Implementation-Version: " + impVersion);
            }
            if (impBuildDate != null)
            {
                System.out.println("Built-Date: " + impBuildDate);
            }
            if (impBuiltBy != null)
            {
                System.out.println("Built-By:   " + impBuiltBy);
            }

            System.exit(0);
        }
        catch (IOException e)
        {            
            System.out.println("Couldn't read manifest.");
        }        
    }

    /**
     * @param args
     */
    public static void main(String[] args)
    {
        packageVersion version = new packageVersion();
        version.printVersion();        
    }

}

Here's the matching build.xml:

<project name="packageVersion" default="run" basedir=".">

    <property name="src" location="src"/>
    <property name="build" location="bin"/>
    <property name="dist" location="dist"/>

    <target name="init">
     <tstamp>
      <format property="TIMESTAMP" pattern="yyyy-MM-dd HH:mm:ss" />
     </tstamp>
        <mkdir dir="${build}"/>
        <mkdir dir="${build}/META-INF"/>
    </target>

    <target name="compile" depends="init">
        <javac debug="on" srcdir="${src}" destdir="${build}"/>
    </target>

    <target name="dist" depends = "compile">  
     <mkdir dir="${dist}"/>  
     <property name="version.num" value="1.0.0"/>
        <buildnumber file="build.num"/>
        <manifest file="${build}/META-INF/MANIFEST.MF">
         <attribute name="Built-By" value="${user.name}" />
         <attribute name="Built-Date" value="${TIMESTAMP}" />                      
            <attribute name="Implementation-Vendor" value="Company" />
         <attribute name="Implementation-Title" value="PackageVersion" />
            <attribute name="Implementation-Version" value="${version.num} (b${build.number})"/>
         <section name="com/company/division/project/packageversion">
          <attribute name="Sealed" value="false"/>
         </section>      
        </manifest>  
     <jar destfile="${dist}/packageversion-${version.num}.jar" basedir="${build}" manifest="${build}/META-INF/MANIFEST.MF"/>       
    </target>

    <target name="clean">
        <delete dir="${build}"/>
        <delete dir="${dist}"/>
    </target>

    <target name="run" depends="dist">      
        <java classname="com.company.division.project.packageversion.packageVersion">
         <arg value="-h"/>
            <classpath>
             <pathelement location="${dist}/packageversion-${version.num}.jar"/>
             <pathelement path="${java.class.path}"/>
            </classpath>
        </java>
    </target>

</project>
A: 

ClassLoader.getResource(String) will load the first manifest it finds on the classpath, which may be the manifest for some other JAR file. Thus, you can either enumerate all the manifests to find the one you want or use some other mechanism, such as a properties file with a unique name.

McDowell
A: 

I've found the comment by McDowell to be true - which MANIFEST.MF file gets picked up depends on the classpath and might not be the one wanted. I use this

String cp = PCAS.class.getResource(PCAS.class.getSimpleName() + ".class").toString();
cp = cp.substring(0, cp.indexOf(PCAS.class.getPackage().getName())) 
              +  "META-INF/MANIFEST.MF";
Manifest mf = new Manifest((new URL(cp)).openStream());

which I adapted from link text

Glenn Burkhardt
A: 

You can get the manifest for an arbitrary class in an arbitrary jar without parsing the class url (which could be brittle). Just locate a resource that you know is in the jar you want, and then cast the connection to JarURLConnection.

If you want the code to work when the class is not bundled in a jar, add an instanceof check on the type of URL connection returned. Classes in an unpacked class hierarchy will return a internal Sun FileURLConnection instead of the JarUrlConnection. Then you can load the Manifest using one of the InputStream methods described in other answers.

@Test
public void testManifest() throws IOException {
    URL res = org.junit.Assert.class.getResource(org.junit.Assert.class.getSimpleName() + ".class");
    JarURLConnection conn = (JarURLConnection) res.openConnection();
    Manifest mf = conn.getManifest();
    Attributes atts = mf.getMainAttributes();
    for (Object v : atts.values()) {
        System.out.println(v);
    }
}
gibbss