I had the need today to write a dummy jar with a manifest in the context of a unit test. Doing so was easy, but I ran into two goofy API problems with the jar manifest writing that tripped me up. Neither is well-documented.
A cleaned up approximation of what I was doing is:
[source:java]
import java.io.FileOutputStream;
import java.util.jar.*;
public class MakeJar {
public static void main(String arg[]) throws Exception {
// Name of jar file to write
String jar = arg[0];
// Read key/value pairs from arg and write as manifest attributes
Manifest manifest = new Manifest();
Attributes attributes = manifest.getMainAttributes();
for (int i = 1; i < arg.length; i = i + 2) { String key = arg[i]; String value = arg[i + 1]; attributes.put(key, value); // #1 } FileOutputStream fstream = new FileOutputStream(jar); JarOutputStream stream = new JarOutputStream(fstream, manifest); stream.flush(); stream.close(); } } [/source] You can run this with something like: java MakeJar dummy.jar a 1 b 2
. This should create dummy.jar and define a manifest with “a: 1” and “b: 2” as main attributes. But if you run the code above, you’ll get a ClassCastException on the line marked “#1”.
The confusion here is because Attributes implements Map<Object,Object>…but also imposes additional constraints on the keys and values without declaring those in the generic types. I’m guessing this lack of specificity was done for backwards-compatibility. Attributes would ideally be declared as a Map<Attributes.Name,String>. So, if you use the normal Map.put() method in Attributes, you will get a ClassCastException if you pass anything other than an Attributes.Name object as the key or a String as the value. Of course, Attributes defines it’s own specialized type-specific method putValue(String name, String value), although you’ll notice even this method does not take an Attributes.Name – it takes a String key which it wraps into a Name.
This constraint is documented in the @throws javadoc for the put() method, but I think some additional words on the parameters stating the required types of the arguments would have been awfully nice.
On a side note, similar problems exist on the get side. The get() method requires an Attribute.Name, but does not document this. If you pass it a String, you will always get null. Two specialized getValue() methods are provided – one that takes a String, and one that takes a Name…which leads me to wonder why there aren’t two versions of putValue… In all, this class does not win the API consistency award.
In any case, we can correct our code by changing the #1 line to be attributes.putValue(key, value);
. If we do this and re-run the program you should see a dummy.jar get created with a META-INF/MANIFEST.MF file, as desired. But, if you actually look at the contents of the manifest you may be surprised as the contents of the manifest file will be empty!
It turns out that there is one required manifest attribute – Attributes.Name.MANIFEST_VERSION. If you fail to set this on the main attributes for the manifest, all attributes are silently ignored. So, a working version of my original code is below with a fixed line #1 and an added line #2.
[source:java]
import java.io.FileOutputStream;
import java.util.jar.*;
public class MakeJar {
public static void main(String arg[]) throws Exception {
// Name of jar file to write
String jar = arg[0];
// Read key/value pairs from arg and write as manifest attributes
Manifest manifest = new Manifest();
Attributes attributes = manifest.getMainAttributes();
for (int i = 1; i < arg.length; i = i + 2) { String key = arg[i]; String value = arg[i + 1]; attributes.putValue(key, value); // #1 } attributes.put(Attributes.Name.MANIFEST_VERSION, “1.0”); // #2 FileOutputStream fstream = new FileOutputStream(jar); JarOutputStream stream = new JarOutputStream(fstream, manifest); stream.flush(); stream.close(); } } [/source] This requirement is documented, but only in the method comments for Manifest.write(OutputStream) and the protected method Attributes.writeMain(). Given that you are actually setting the attributes on the Attributes class, I think this is really worth a mention in the class docs or somewhere more prominent! Or even better, you could set a default to use. AFAIK, “1.0” is the only valid version - why not make that the default? Or throw an IllegalStateException if attributes have been set but no manifest version has been set and the main attributes are being written. Surely, silently ignoring the attributes is the worst possible choice.