1 /*
   2  * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 import java.io.File;
  25 import java.io.IOException;
  26 import java.io.OutputStream;
  27 import java.nio.file.Files;
  28 import java.nio.file.Path;
  29 import java.nio.file.Paths;
  30 import java.nio.file.StandardCopyOption;
  31 import java.util.ArrayList;
  32 import java.util.Enumeration;
  33 import java.util.List;
  34 import java.util.Set;
  35 import java.util.jar.JarEntry;
  36 import java.util.jar.JarFile;
  37 import java.util.jar.JarOutputStream;
  38 import java.util.jar.Manifest;
  39 import java.util.stream.Collectors;
  40 import java.util.stream.Stream;
  41 
  42 /**
  43  * This class consists exclusively of static utility methods that are useful
  44  * for creating and manipulating JAR files.
  45  */
  46 
  47 public final class JarUtils {
  48     private JarUtils() { }
  49 
  50     /**
  51      * Creates a JAR file.
  52      *
  53      * Equivalent to {@code jar cfm <jarfile> <manifest> -C <dir> file...}
  54      *
  55      * The input files are resolved against the given directory. Any input
  56      * files that are directories are processed recursively.
  57      */
  58     public static void createJarFile(Path jarfile, Manifest man, Path dir, Path... file)
  59         throws IOException
  60     {
  61         // create the target directory
  62         Path parent = jarfile.getParent();
  63         if (parent != null)
  64             Files.createDirectories(parent);
  65 
  66         List<Path> entries = new ArrayList<>();
  67         for (Path entry : file) {
  68             Files.find(dir.resolve(entry), Integer.MAX_VALUE,
  69                         (p, attrs) -> attrs.isRegularFile())
  70                     .map(e -> dir.relativize(e))
  71                     .forEach(entries::add);
  72         }
  73 
  74         try (OutputStream out = Files.newOutputStream(jarfile);
  75              JarOutputStream jos = new JarOutputStream(out))
  76         {
  77             if (man != null) {
  78                 JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
  79                 jos.putNextEntry(je);
  80                 man.write(jos);
  81                 jos.closeEntry();
  82             }
  83 
  84             for (Path entry : entries) {
  85                 String name = toJarEntryName(entry);
  86                 jos.putNextEntry(new JarEntry(name));
  87                 Files.copy(dir.resolve(entry), jos);
  88                 jos.closeEntry();
  89             }
  90         }
  91     }
  92 
  93     /**
  94      * Creates a JAR file.
  95      *
  96      * Equivalent to {@code jar cf <jarfile>  -C <dir> file...}
  97      *
  98      * The input files are resolved against the given directory. Any input
  99      * files that are directories are processed recursively.
 100      */
 101     public static void createJarFile(Path jarfile, Path dir, Path... file)
 102         throws IOException
 103     {
 104         createJarFile(jarfile, null, dir, file);
 105     }
 106 
 107     /**
 108      * Creates a JAR file.
 109      *
 110      * Equivalent to {@code jar cf <jarfile> -C <dir> file...}
 111      *
 112      * The input files are resolved against the given directory. Any input
 113      * files that are directories are processed recursively.
 114      */
 115     public static void createJarFile(Path jarfile, Path dir, String... input)
 116         throws IOException
 117     {
 118         Path[] paths = Stream.of(input).map(Paths::get).toArray(Path[]::new);
 119         createJarFile(jarfile, dir, paths);
 120     }
 121 
 122     /**
 123      * Creates a JAR file from the contents of a directory.
 124      *
 125      * Equivalent to {@code jar cf <jarfile> -C <dir> .}
 126      */
 127     public static void createJarFile(Path jarfile, Path dir) throws IOException {
 128         createJarFile(jarfile, dir, Paths.get("."));
 129     }
 130 
 131     /**
 132      * Update a JAR file.
 133      *
 134      * Equivalent to {@code jar uf <jarfile> -C <dir> file...}
 135      *
 136      * The input files are resolved against the given directory. Any input
 137      * files that are directories are processed recursively.
 138      */
 139     public static void updateJarFile(Path jarfile, Path dir, Path... file)
 140         throws IOException
 141     {
 142         List<Path> entries = new ArrayList<>();
 143         for (Path entry : file) {
 144             Files.find(dir.resolve(entry), Integer.MAX_VALUE,
 145                     (p, attrs) -> attrs.isRegularFile())
 146                     .map(e -> dir.relativize(e))
 147                     .forEach(entries::add);
 148         }
 149 
 150         Set<String> names = entries.stream()
 151                 .map(JarUtils::toJarEntryName)
 152                 .collect(Collectors.toSet());
 153 
 154         Path tmpfile = Files.createTempFile("jar", "jar");
 155 
 156         try (OutputStream out = Files.newOutputStream(tmpfile);
 157              JarOutputStream jos = new JarOutputStream(out))
 158         {
 159             // copy existing entries from the original JAR file
 160             try (JarFile jf = new JarFile(jarfile.toString())) {
 161                 Enumeration<JarEntry> jentries = jf.entries();
 162                 while (jentries.hasMoreElements()) {
 163                     JarEntry jentry = jentries.nextElement();
 164                     if (!names.contains(jentry.getName())) {
 165                         jos.putNextEntry(jentry);
 166                         jf.getInputStream(jentry).transferTo(jos);
 167                     }
 168                 }
 169             }
 170 
 171             // add the new entries
 172             for (Path entry : entries) {
 173                 String name = toJarEntryName(entry);
 174                 jos.putNextEntry(new JarEntry(name));
 175                 Files.copy(dir.resolve(entry), jos);
 176             }
 177         }
 178 
 179         // replace the original JAR file
 180         Files.move(tmpfile, jarfile, StandardCopyOption.REPLACE_EXISTING);
 181     }
 182 
 183     /**
 184      * Update a JAR file.
 185      *
 186      * Equivalent to {@code jar uf <jarfile> -C <dir> .}
 187      */
 188     public static void updateJarFile(Path jarfile, Path dir) throws IOException {
 189         updateJarFile(jarfile, dir, Paths.get("."));
 190     }
 191 
 192 
 193     /**
 194      * Map a file path to the equivalent name in a JAR file
 195      */
 196     private static String toJarEntryName(Path file) {
 197         Path normalized = file.normalize();
 198         return normalized.subpath(0, normalized.getNameCount())  // drop root
 199                 .toString()
 200                 .replace(File.separatorChar, '/');
 201     }
 202 }