63

How does one create a JAR file programmatically using java.util.jar.JarOutputStream? The JAR file produced by my program looks correct (it extracts fine) but when I try loading a library from it Java complains that it cannot find files which are clearly stored inside it. If I extract the JAR file and use Sun's jar command-line tool to re-compress it the resulting library works fine. In short, something is wrong with my JAR file.

Please explain how to create a JAR file programmatically, complete with a manifest file.

Gili
  • 86,244
  • 97
  • 390
  • 689

6 Answers6

107

It turns out that JarOutputStream has three undocumented quirks:

  1. Directory names must end with a '/' slash.
  2. Paths must use '/' slashes, not '\'
  3. Entries may not begin with a '/' slash.

Here is the correct way to create a Jar file:

public void run() throws IOException {
    Manifest manifest = new Manifest();
    manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
    JarOutputStream target = new JarOutputStream(new FileOutputStream("output.jar"), manifest);
    add(new File("inputDirectory"), target);
    target.close();
}

private void add(File source, JarOutputStream target) throws IOException {
    String name = source.getPath().replace("\\", "/");
    if (source.isDirectory()) {
        if (!name.endsWith("/")) {
            name += "/";
        }
        JarEntry entry = new JarEntry(name);
        entry.setTime(source.lastModified());
        target.putNextEntry(entry);
        target.closeEntry();
        for (File nestedFile : source.listFiles()) {
            add(nestedFile, target);
        }
    } else {
        JarEntry entry = new JarEntry(name);
        entry.setTime(source.lastModified());
        target.putNextEntry(entry);
        try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(source))) {
            byte[] buffer = new byte[1024];
            while (true) {
                int count = in.read(buffer);
                if (count == -1)
                    break;
                target.write(buffer, 0, count);
            }
            target.closeEntry();
        }
    }
}
Sergey
  • 3,253
  • 2
  • 33
  • 55
Gili
  • 86,244
  • 97
  • 390
  • 689
  • 19
    these 'quirks' are actually part of the zip specification (jar files are just zip files with a manifest and a different extension). I agree that it should be documented in the API docs, though - I suggest opening an issue (http://bugs.sun.com/bugdatabase/) – Kevin Day Aug 15 '09 at 19:18
  • 6
    More importantly, the API should prevent you from creating invalid ZIP/JAR files by throwing exceptions if you pass in the wrong type of slash or by converting them automatically. With respect to directories ending with a slash, it should definitely be documented since there is no way to correct it automatically. I filed a bug report but it hasn't been accepted yet. – Gili Aug 16 '09 at 15:52
  • Classic Sun/Oracle. Closed as "not a bug": http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6873352 – Gili Jan 17 '12 at 18:06
  • 2
    FYI - Zip spec: http://www.pkware.com/documents/casestudies/APPNOTE.TXT - search for "file name: (Variable)". – David Carboni Apr 03 '12 at 10:52
  • 1
    @Gili The api cannot "prevent" you from using the "wrong" slash; because, the "wrong" slash is a valid character in the file name. Not all operating systems recognize "\" as a directory separator, and those that do not allow (no-directory including) files names of "he\he\he". – Edwin Buck Apr 30 '12 at 14:22
  • in this call `add(new File("inputDirectory"), target);` where should `inputDirectory` be pointing? project root, or something lower in the directory hierarchy? – Alex Averbuch May 11 '12 at 08:32
  • @AlexAverbuch, if I recall correctly, it should be pointing to the project root. – Gili May 23 '13 at 03:01
  • 1
    This program result Strange output $ ls MyClass.class output.jar using this program i have created jar output.jar $ jar tf output.jar META-INF/MANIFEST.MF /Users/aninath/Documents/workspace/interview/.metadata/.plugins/org.eclipse.wst.server.core/tmp0/work/Catalina/localhost/mywebapp2/tmp/ /Users/aninath/Documents/workspace/interview/.metadata/.plugins/org.eclipse.wst.server.core/tmp0/work/Catalina/localhost/mywebapp2/tmp/MyClass.class /Users/aninath/Documents/workspace/interview/.metadata/.plugins/org.eclipse.wst.server.core/tmp0/work/Catalina/localhost/mywebapp2/tmp/output.jar – anish Feb 28 '14 at 19:07
  • @anish: post a separate question. Comments aren't appropriate for this kind a question. – Gili Mar 04 '14 at 23:00
  • 1
    In my jar there are the class files listed with the classdirectory as prefix followed by the package structure. Is it possible to just have the package structure inside the jar somehow? – Gobliins Oct 26 '15 at 14:01
  • @Gobliins You need to post a separate question. Comments aren't appropriate for this kind of question. – Gili Oct 27 '15 at 03:25
  • 1
    @anish @Gobliins Yeah, it's not quite right - there's a fix: if you add a String param "parents", then change the `name` assignment to `(parents + source.getName()).replace("\\", "/")`, add `name` to the recursive `add` call, and make a wrapper function that loops through the children of the original File, on each calling `add("", nestedFile, target);`, it gives the right relative paths. Sorry if that's confusing; I don't have enough room to post all of both methods here. – Erhannis Jul 14 '18 at 20:51
  • @Gili Those were comments saying that the solution had a bug. Why is a comment not the appropriate place for that? – Erhannis Jul 14 '18 at 20:54
  • @Erhannis It's not a bug per-se because this isn't the focus of the question. The focus of this question is how to create a JAR file using `JarOutputStream` and the answer does that. The implementation assumes that you want to pass the entire source path into the JAR file but this is something that you can easily modify. – Gili Jul 16 '18 at 01:46
  • @Erhannis can you please post the answer that solves this relative path problem? I really need it. – Shehan Dhaleesha Dec 13 '19 at 04:40
  • @ShehanDhaleesha I've now posted an edited version of the code. Should show up below, but here's a direct link: https://stackoverflow.com/a/59439786/513038 – Erhannis Dec 21 '19 at 20:58
10

There's another "quirk" to pay attention: All JarEntry's names should NOT begin with "/".

For example: The jar entry name for the manifest file is "META-INF/MANIFEST.MF" and not "/META-INF/MANIFEST.MF".

The same rule should be followed for all jar entries.

  • 1
    This should be posted as a comment to the accepted answer because it doesn't answer the main question. – cdalxndr Aug 26 '19 at 11:28
3

Ok, so by request, here's Gili's code, modified to use relative paths rather than absolute ones. (Replace "inputDirectory" with the directory of your choice.) I just tested it, but if it doesn't work, lemme know.

   public void run() throws IOException
   {
      Manifest manifest = new Manifest();
      manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
      JarOutputStream target = new JarOutputStream(new FileOutputStream("output.jar"), manifest);
      File inputDirectory = new File("inputDirectory");
      for (File nestedFile : inputDirectory.listFiles())
         add("", nestedFile, target);
      target.close();
   }

   private void add(String parents, File source, JarOutputStream target) throws IOException
   {
      BufferedInputStream in = null;
      try
      {
         String name = (parents + source.getName()).replace("\\", "/");

         if (source.isDirectory())
         {
            if (!name.isEmpty())
            {
               if (!name.endsWith("/"))
                  name += "/";
               JarEntry entry = new JarEntry(name);
               entry.setTime(source.lastModified());
               target.putNextEntry(entry);
               target.closeEntry();
            }
            for (File nestedFile : source.listFiles())
               add(name, nestedFile, target);
            return;
         }

         JarEntry entry = new JarEntry(name);
         entry.setTime(source.lastModified());
         target.putNextEntry(entry);
         in = new BufferedInputStream(new FileInputStream(source));

         byte[] buffer = new byte[1024];
         while (true)
         {
            int count = in.read(buffer);
            if (count == -1)
               break;
            target.write(buffer, 0, count);
         }
         target.closeEntry();
      }
      finally
      {
         if (in != null)
            in.close();
      }
   }
Erhannis
  • 4,256
  • 4
  • 34
  • 48
1

You can do it with this code:

public void write(File[] files, String comment) throws IOException {
    FileOutputStream fos = new FileOutputStream(PATH + FILE);
    JarOutputStream jos = new JarOutputStream(fos, manifest);
    BufferedOutputStream bos = new BufferedOutputStream(jos);
    jos.setComment(comment);
    for (File f : files) {
        print("Writing file: " + f.toString());
        BufferedReader br = new BufferedReader(new FileReader(f));
        jos.putNextEntry(new JarEntry(f.getName()));
        int c;
        while ((c = br.read()) != -1) {
            bos.write(c);
        }
        br.close();
        bos.flush();
    }
    bos.close();
//  JarOutputStream jor = new JarOutputStream(new FileOutputStream(PATH + FILE), manifest);

}

PATH variable: path to JAR file

FILE variable: name and format

ceph3us
  • 7,326
  • 3
  • 36
  • 43
Muskovets
  • 449
  • 8
  • 16
  • 1
    What does this add that the accepted answer does not already say? – Gili Oct 13 '16 at 12:36
  • The accepted answer use more code than my. I think, you don't want to write very much code when you can write a bit. – Muskovets Dec 10 '17 at 18:06
  • 4
    In all due fairness, your answer contains less code because it does less. The accepted answer contains code that massages the input to the format expected by JarOutputStream . Your code will fail silently if you run under Windows or if directory names do not end with a slash. – Gili Dec 12 '17 at 02:03
  • The accepted answer also includes the manifest and the top-level directory entry. (I agree it could be more terse, though.) – David Moles Sep 05 '18 at 19:17
  • 1
    Getting ClassFormatError with this - using the accepted answer works though. – Kartik Chugh Aug 16 '19 at 17:43
  • 1
    The streams will not be closed if an exception is thrown in-between. You must use try-finally or try-with-resources to guarantee that the streams will be closed. – cdalxndr Aug 26 '19 at 11:32
1

This answer would solve the relative path problem.

private static void createJar(File source, JarOutputStream target) {
        createJar(source, source, target);
    }

    private static void createJar(File source, File baseDir, JarOutputStream target) {
        BufferedInputStream in = null;

        try {
            if (!source.exists()){
                throw new IOException("Source directory is empty");
            }
            if (source.isDirectory()) {
                // For Jar entries, all path separates should be '/'(OS independent)
                String name = source.getPath().replace("\\", "/");
                if (!name.isEmpty()) {
                    if (!name.endsWith("/")) {
                        name += "/";
                    }
                    JarEntry entry = new JarEntry(name);
                    entry.setTime(source.lastModified());
                    target.putNextEntry(entry);
                    target.closeEntry();
                }
                for (File nestedFile : source.listFiles()) {
                    createJar(nestedFile, baseDir, target);
                }
                return;
            }

            String entryName = baseDir.toPath().relativize(source.toPath()).toFile().getPath().replace("\\", "/");
            JarEntry entry = new JarEntry(entryName);
            entry.setTime(source.lastModified());
            target.putNextEntry(entry);
            in = new BufferedInputStream(new FileInputStream(source));

            byte[] buffer = new byte[1024];
            while (true) {
                int count = in.read(buffer);
                if (count == -1)
                    break;
                target.write(buffer, 0, count);
            }
            target.closeEntry();
        } catch (Exception ignored) {

        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (Exception ignored) {
                    throw new RuntimeException(ignored);
                }
            }
        }
    }
Shehan Dhaleesha
  • 627
  • 1
  • 10
  • 30
0

Here's some sample code for creating a JAR file using the JarOutputStream:

ars
  • 120,335
  • 23
  • 147
  • 134
  • 1
    I'm already doing this. In fact, the example you referenced to fails to point out that one must explicitly putNextEntry() on directory names or invoke JarOutputStream.closeEntry(). Something else must be wrong. – Gili Aug 15 '09 at 05:39
  • Ah, OK. It was a little hard to offer a better solution without seeing any code, so I just pointed you at that reference. Glad you figured it out though. – ars Aug 15 '09 at 07:53