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 package jdk.test.lib.util;
  25 
  26 import java.io.ByteArrayOutputStream;
  27 import java.io.File;
  28 import java.io.FileInputStream;
  29 import java.io.FileNotFoundException;
  30 import java.io.FileOutputStream;
  31 import java.io.IOException;
  32 import java.io.InputStream;
  33 import java.io.OutputStream;
  34 import java.nio.file.Files;
  35 import java.nio.file.InvalidPathException;
  36 import java.nio.file.Path;
  37 import java.nio.file.Paths;
  38 import java.nio.file.StandardCopyOption;
  39 import java.util.ArrayList;
  40 import java.util.Enumeration;
  41 import java.util.HashMap;
  42 import java.util.List;
  43 import java.util.Map;
  44 import java.util.Objects;
  45 import java.util.Set;
  46 import java.util.jar.JarEntry;
  47 import java.util.jar.JarFile;
  48 import java.util.jar.JarOutputStream;
  49 import java.util.jar.Manifest;
  50 import java.util.stream.Collectors;
  51 import java.util.stream.Stream;
  52 
  53 /**
  54  * This class consists exclusively of static utility methods that are useful
  55  * for creating and manipulating JAR files.
  56  */
  57 public final class JarUtils {
  58     private JarUtils() { }
  59 
  60     /**
  61      * Creates a JAR file.
  62      *
  63      * Equivalent to {@code jar cfm <jarfile> <manifest> -C <dir> file...}
  64      *
  65      * The input files are resolved against the given directory. Any input
  66      * files that are directories are processed recursively.
  67      */
  68     public static void createJarFile(Path jarfile, Manifest man, Path dir, Path... files)
  69             throws IOException
  70     {
  71         // create the target directory
  72         Path parent = jarfile.getParent();
  73         if (parent != null) {
  74             Files.createDirectories(parent);
  75         }
  76 
  77         List<Path> entries = findAllRegularFiles(dir, files);
  78 
  79         try (OutputStream out = Files.newOutputStream(jarfile);
  80              JarOutputStream jos = new JarOutputStream(out)) {
  81             if (man != null) {
  82                 JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
  83                 jos.putNextEntry(je);
  84                 man.write(jos);
  85                 jos.closeEntry();
  86             }
  87 
  88             for (Path entry : entries) {
  89                 String name = toJarEntryName(entry);
  90                 jos.putNextEntry(new JarEntry(name));
  91                 Files.copy(dir.resolve(entry), jos);
  92                 jos.closeEntry();
  93             }
  94         }
  95     }
  96 
  97     /**
  98      * Creates a JAR file.
  99      *
 100      * Equivalent to {@code jar cf <jarfile>  -C <dir> file...}
 101      *
 102      * The input files are resolved against the given directory. Any input
 103      * files that are directories are processed recursively.
 104      */
 105     public static void createJarFile(Path jarfile, Path dir, Path... files)
 106             throws IOException
 107     {
 108         createJarFile(jarfile, null, dir, files);
 109     }
 110 
 111     /**
 112      * Creates a JAR file from the contents of a directory.
 113      *
 114      * Equivalent to {@code jar cf <jarfile> -C <dir> .}
 115      */
 116     public static void createJarFile(Path jarfile, Path dir) throws IOException {
 117         createJarFile(jarfile, dir, Paths.get("."));
 118     }
 119 
 120 
 121     /**
 122      * Creates a JAR file.
 123      *
 124      * Equivalent to {@code jar cf <jarfile> -C <dir> file...}
 125      *
 126      * The input files are resolved against the given directory. Any input
 127      * files that are directories are processed recursively.
 128      */
 129     public static void createJarFile(Path jarfile, Path dir, String... input)
 130             throws IOException
 131     {
 132         Path[] paths = Stream.of(input).map(Paths::get).toArray(Path[]::new);
 133         createJarFile(jarfile, dir, paths);
 134     }
 135 
 136     /**
 137      * Updates a JAR file.
 138      *
 139      * Equivalent to {@code jar uf <jarfile> -C <dir> file...}
 140      *
 141      * The input files are resolved against the given directory. Any input
 142      * files that are directories are processed recursively.
 143      */
 144     public static void updateJarFile(Path jarfile, Path dir, Path... files)
 145             throws IOException
 146     {
 147         List<Path> entries = findAllRegularFiles(dir, files);
 148 
 149         Set<String> names = entries.stream()
 150                                    .map(JarUtils::toJarEntryName)
 151                                    .collect(Collectors.toSet());
 152 
 153         Path tmpfile = Files.createTempFile("jar", "jar");
 154 
 155         try (OutputStream out = Files.newOutputStream(tmpfile);
 156              JarOutputStream jos = new JarOutputStream(out)) {
 157             // copy existing entries from the original JAR file
 158             try (JarFile jf = new JarFile(jarfile.toString())) {
 159                 Enumeration<JarEntry> jentries = jf.entries();
 160                 while (jentries.hasMoreElements()) {
 161                     JarEntry jentry = jentries.nextElement();
 162                     if (!names.contains(jentry.getName())) {
 163                         jos.putNextEntry(jentry);
 164                         transferStream(jf.getInputStream(jentry), jos);
 165                     }
 166                 }
 167             }
 168 
 169             // add the new entries
 170             for (Path entry : entries) {
 171                 String name = toJarEntryName(entry);
 172                 jos.putNextEntry(new JarEntry(name));
 173                 Files.copy(dir.resolve(entry), jos);
 174             }
 175         }
 176 
 177         // replace the original JAR file
 178         Files.move(tmpfile, jarfile, StandardCopyOption.REPLACE_EXISTING);
 179     }
 180 
 181     /**
 182      * Updates a JAR file.
 183      *
 184      * Equivalent to {@code jar uf <jarfile> -C <dir> .}
 185      */
 186     public static void updateJarFile(Path jarfile, Path dir) throws IOException {
 187         updateJarFile(jarfile, dir, Paths.get("."));
 188     }
 189 
 190 
 191     /**
 192      * Create jar file with specified files. If a specified file does not exist,
 193      * a new jar entry will be created with the file name itself as the content.
 194      */
 195     @Deprecated
 196     public static void createJar(String dest, String... files)
 197             throws IOException {
 198         try (JarOutputStream jos = new JarOutputStream(
 199                 new FileOutputStream(dest), new Manifest())) {
 200             for (String file : files) {
 201                 System.out.println(String.format("Adding %s to %s",
 202                         file, dest));
 203 
 204                 // add an archive entry, and write a file
 205                 jos.putNextEntry(new JarEntry(file));
 206                 try (FileInputStream fis = new FileInputStream(file)) {
 207                     transferStream(fis, jos);
 208                 } catch (FileNotFoundException e) {
 209                     jos.write(file.getBytes());
 210                 }
 211             }
 212         }
 213         System.out.println();
 214     }
 215 
 216     /**
 217      * Add or remove specified files to existing jar file. If a specified file
 218      * to be updated or added does not exist, the jar entry will be created
 219      * with the file name itself as the content.
 220      *
 221      * @param src the original jar file name
 222      * @param dest the new jar file name
 223      * @param files the files to update. The list is broken into 2 groups
 224      *              by a "-" string. The files before in the 1st group will
 225      *              be either updated or added. The files in the 2nd group
 226      *              will be removed. If no "-" exists, all files belong to
 227      *              the 1st group.
 228      * @throws IOException if there is an error
 229      */
 230     @Deprecated
 231     public static void updateJar(String src, String dest, String... files)
 232             throws IOException {
 233         Map<String,Object> changes = new HashMap<>();
 234         boolean update = true;
 235         for (String file : files) {
 236             if (file.equals("-")) {
 237                 update = false;
 238             } else if (update) {
 239                 try {
 240                     Path p = Paths.get(file);
 241                     if (Files.exists(p)) {
 242                         changes.put(file, p);
 243                     } else {
 244                         changes.put(file, file);
 245                     }
 246                 } catch (InvalidPathException e) {
 247                     // Fallback if file not a valid Path.
 248                     changes.put(file, file);
 249                 }
 250             } else {
 251                 changes.put(file, Boolean.FALSE);
 252             }
 253         }
 254         updateJar(src, dest, changes);
 255     }
 256 
 257     /**
 258      * Update content of a jar file.
 259      *
 260      * @param src the original jar file name
 261      * @param dest the new jar file name
 262      * @param changes a map of changes, key is jar entry name, value is content.
 263      *                Value can be Path, byte[] or String. If key exists in
 264      *                src but value is Boolean FALSE. The entry is removed.
 265      *                Existing entries in src not a key is unmodified.
 266      * @throws IOException if there is an error
 267      */
 268     @Deprecated
 269     public static void updateJar(String src, String dest,
 270                                  Map<String,Object> changes)
 271             throws IOException {
 272 
 273         // What if input changes is immutable?
 274         changes = new HashMap<>(changes);
 275 
 276         System.out.printf("Creating %s from %s...\n", dest, src);
 277 
 278         if (dest.equals(src)) {
 279             throw new IOException("src and dest cannot be the same");
 280         }
 281 
 282         try (JarOutputStream jos = new JarOutputStream(
 283                 new FileOutputStream(dest))) {
 284 
 285             try (JarFile srcJarFile = new JarFile(src)) {
 286                 Enumeration<JarEntry> entries = srcJarFile.entries();
 287                 while (entries.hasMoreElements()) {
 288                     JarEntry entry = entries.nextElement();
 289                     String name = entry.getName();
 290                     if (changes.containsKey(name)) {
 291                         System.out.println(String.format("- Update %s", name));
 292                         updateEntry(jos, name, changes.get(name));
 293                         changes.remove(name);
 294                     } else {
 295                         System.out.println(String.format("- Copy %s", name));
 296                         jos.putNextEntry(entry);
 297                         transferStream(srcJarFile.getInputStream(entry), jos);
 298                     }
 299                 }
 300             }
 301             for (Map.Entry<String, Object> e : changes.entrySet()) {
 302                 System.out.println(String.format("- Add %s", e.getKey()));
 303                 updateEntry(jos, e.getKey(), e.getValue());
 304             }
 305         }
 306         System.out.println();
 307     }
 308 
 309     /**
 310      * Update the Manifest inside a jar.
 311      *
 312      * @param src the original jar file name
 313      * @param dest the new jar file name
 314      * @param man the Manifest
 315      *
 316      * @throws IOException
 317      */
 318     public static void updateManifest(String src, String dest, Manifest man)
 319             throws IOException {
 320         ByteArrayOutputStream bout = new ByteArrayOutputStream();
 321         man.write(bout);
 322         Map map = new HashMap<>();
 323         map.put(JarFile.MANIFEST_NAME, bout.toByteArray());
 324         updateJar(src, dest, map);
 325     }
 326 
 327     private static void updateEntry(JarOutputStream jos, String name, Object content)
 328            throws IOException {
 329         if (content instanceof Boolean) {
 330             if (((Boolean) content).booleanValue()) {
 331                 throw new RuntimeException("Boolean value must be FALSE");
 332             }
 333         } else {
 334             jos.putNextEntry(new JarEntry(name));
 335             if (content instanceof Path) {
 336                 transferStream(Files.newInputStream((Path) content), jos);
 337             } else if (content instanceof byte[]) {
 338                 jos.write((byte[]) content);
 339             } else if (content instanceof String) {
 340                 jos.write(((String) content).getBytes());
 341             } else {
 342                 throw new RuntimeException("Unknown type " + content.getClass());
 343             }
 344         }
 345     }
 346 
 347     /**
 348      * Maps a file path to the equivalent name in a JAR file
 349      */
 350     private static String toJarEntryName(Path file) {
 351         Path normalized = file.normalize();
 352         return normalized.subpath(0, normalized.getNameCount())  // drop root
 353                          .toString()
 354                          .replace(File.separatorChar, '/');
 355     }
 356 
 357     private static List<Path> findAllRegularFiles(Path dir, Path[] files) throws IOException {
 358         List<Path> entries = new ArrayList<>();
 359         for (Path file : files) {
 360             try (Stream<Path> stream = Files.find(dir.resolve(file), Integer.MAX_VALUE,
 361                     (p, attrs) -> attrs.isRegularFile())) {
 362                 stream.map(dir::relativize)
 363                       .forEach(entries::add);
 364             }
 365         }
 366         return entries;
 367     }
 368     
 369     private static long transferStream(InputStream in, OutputStream out) throws IOException {
 370         final int DEFAULT_BUFFER_SIZE = 8192;
 371         Objects.requireNonNull(out, "out");
 372         long transferred = 0;
 373         byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
 374         int read;
 375         while ((read = in.read(buffer, 0, DEFAULT_BUFFER_SIZE)) >= 0) {
 376             out.write(buffer, 0, read);
 377             transferred += read;
 378         }
 379         return transferred;
 380     }
 381 }