How do you create a MANIFEST.MF that's available when you're testing and running from a jar in production?

27,649

Solution 1

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);
    }
}

Solution 2

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.

Solution 3

You want to use this:

Enumeration<URL> resources = Thread.currentThread().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.

Solution 4

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>

Solution 5

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.

Share:
27,649
user16216
Author by

user16216

Updated on July 09, 2022

Comments

  • user16216
    user16216 almost 2 years

    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?

  • metamatt
    metamatt about 13 years
    You're right that getResource() often finds the wrong manifest; I'm experiencing that; I tried your link to ClassLoader.getResources() to enumerate all manifests, but getResources("/META-INF/MANIFEST.MF") returns nothing, and getResources("") returns more than nothing but less than useful. I suspect I'm using the wrong class loader, but then "enumerate all manifests" devolves to "enumerate all ClassLoaders"!
  • metamatt
    metamatt about 13 years
    This almost works for me, though the link to where you got this from is broken now. I say "almost" because Class.getPackage() returns a dot-separated name (org.foo.bar), and Class.getSimpleName() returns a slash-separated name (org/foo/bar). Because of this, I like gibbss' answer which avoids having to parse the class url.
  • metamatt
    metamatt about 13 years
    Nice, thanks. I tried everything here, and of the (currently 3) answers that acknowledge that Class.getResource() often finds the wrong manifest (in the wrong jar) and offer a solution to find the right manifest, I like this one best.
  • zaid hussian
    zaid hussian over 12 years
    In a real-world application that uses any third-party libraries, this is exceedingly likely to return the wrong manifest.
  • TWiStErRob
    TWiStErRob almost 9 years
    If you want something specific use atts.getValue("Attribute-Name") or atts.get(new Attributes.Name("Attribute-Name")), non-generic code FTW!
  • Carlos
    Carlos about 8 years
    Class name should begin in uppercase. It should be PackageVersion instead of packageVersion.
  • Hiran Chaudhuri
    Hiran Chaudhuri over 3 years
    The solution shown above only works with a single manifest or at least unique attribute values across the manifest files. Just suppose the build date is contained in several JARs and as you say all of them are read. Which of the 'build date' attributes will be returned?
  • Hiran Chaudhuri
    Hiran Chaudhuri over 3 years
    This is problematic if on the ContextClassLoader's classpath you have multiple sources with manifest files.
  • Mario
    Mario over 3 years
    FYI, if used from within an IDE e.g. for testing, the type cast to JarURLConnection throws an exception with the bootstrap class loader. However, once the class is included in the built JAR, it works. Thanks.