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