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 }