1 /* 2 * Copyright (c) 2013, 2016, 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 toolbox; 25 26 import java.io.BufferedInputStream; 27 import java.io.ByteArrayInputStream; 28 import java.io.File; 29 import java.io.IOError; 30 import java.io.IOException; 31 import java.io.InputStream; 32 import java.io.OutputStream; 33 import java.net.URI; 34 import java.nio.file.FileVisitResult; 35 import java.nio.file.Files; 36 import java.nio.file.Path; 37 import java.nio.file.Paths; 38 import java.nio.file.SimpleFileVisitor; 39 import java.nio.file.attribute.BasicFileAttributes; 40 import java.util.ArrayList; 41 import java.util.Arrays; 42 import java.util.Collections; 43 import java.util.EnumSet; 44 import java.util.HashMap; 45 import java.util.LinkedHashSet; 46 import java.util.List; 47 import java.util.ListIterator; 48 import java.util.Map; 49 import java.util.Set; 50 import java.util.jar.Attributes; 51 import java.util.jar.JarEntry; 52 import java.util.jar.JarOutputStream; 53 import java.util.jar.Manifest; 54 import java.util.regex.Matcher; 55 import java.util.regex.Pattern; 56 import java.util.stream.Collectors; 57 import java.util.stream.Stream; 58 import javax.tools.FileObject; 59 import javax.tools.JavaFileManager; 60 import javax.tools.JavaFileObject; 61 import static toolbox.ToolBox.currDir; 62 63 /** 64 * A task to configure and run the jar file utility. 65 */ 66 public class JarTask extends AbstractTask<JarTask> { 67 private Path jar; 68 private Manifest manifest; 69 private String classpath; 70 private String mainClass; 71 private Path baseDir; 72 private List<Path> paths; 73 private Set<FileObject> fileObjects; 74 75 /** 76 * Creates a task to write jar files, using API mode. 77 * @param toolBox the {@code ToolBox} to use 78 */ 79 public JarTask(ToolBox toolBox) { 80 super(toolBox, Task.Mode.API); 81 paths = Collections.emptyList(); 82 fileObjects = new LinkedHashSet<>(); 83 } 84 85 /** 86 * Creates a JarTask for use with a given jar file. 87 * @param toolBox the {@code ToolBox} to use 88 * @param path the file 89 */ 90 public JarTask(ToolBox toolBox, String path) { 91 this(toolBox); 92 jar = Paths.get(path); 93 } 94 95 /** 96 * Creates a JarTask for use with a given jar file. 97 * @param toolBox the {@code ToolBox} to use 98 * @param path the file 99 */ 100 public JarTask(ToolBox toolBox, Path path) { 101 this(toolBox); 102 jar = path; 103 } 104 105 /** 106 * Sets a manifest for the jar file. 107 * @param manifest the manifest 108 * @return this task object 109 */ 110 public JarTask manifest(Manifest manifest) { 111 this.manifest = manifest; 112 return this; 113 } 114 115 /** 116 * Sets a manifest for the jar file. 117 * @param manifest a string containing the contents of the manifest 118 * @return this task object 119 * @throws IOException if there is a problem creating the manifest 120 */ 121 public JarTask manifest(String manifest) throws IOException { 122 this.manifest = new Manifest(new ByteArrayInputStream(manifest.getBytes())); 123 return this; 124 } 125 126 /** 127 * Sets the classpath to be written to the {@code Class-Path} 128 * entry in the manifest. 129 * @param classpath the classpath 130 * @return this task object 131 */ 132 public JarTask classpath(String classpath) { 133 this.classpath = classpath; 134 return this; 135 } 136 137 /** 138 * Sets the class to be written to the {@code Main-Class} 139 * entry in the manifest.. 140 * @param mainClass the name of the main class 141 * @return this task object 142 */ 143 public JarTask mainClass(String mainClass) { 144 this.mainClass = mainClass; 145 return this; 146 } 147 148 /** 149 * Sets the base directory for files to be written into the jar file. 150 * @param baseDir the base directory 151 * @return this task object 152 */ 153 public JarTask baseDir(String baseDir) { 154 this.baseDir = Paths.get(baseDir); 155 return this; 156 } 157 158 /** 159 * Sets the base directory for files to be written into the jar file. 160 * @param baseDir the base directory 161 * @return this task object 162 */ 163 public JarTask baseDir(Path baseDir) { 164 this.baseDir = baseDir; 165 return this; 166 } 167 168 /** 169 * Sets the files to be written into the jar file. 170 * @param files the files 171 * @return this task object 172 */ 173 public JarTask files(String... files) { 174 this.paths = Stream.of(files) 175 .map(file -> Paths.get(file)) 176 .collect(Collectors.toList()); 177 return this; 178 } 179 180 /** 181 * Adds a set of file objects to be written into the jar file, by copying them 182 * from a Location in a JavaFileManager. 183 * The file objects to be written are specified by a series of paths; 184 * each path can be in one of the following forms: 185 * <ul> 186 * <li>The name of a class. For example, java.lang.Object. 187 * In this case, the corresponding .class file will be written to the jar file. 188 * <li>the name of a package followed by {@code .*}. For example, {@code java.lang.*}. 189 * In this case, all the class files in the specified package will be written to 190 * the jar file. 191 * <li>the name of a package followed by {@code .**}. For example, {@code java.lang.**}. 192 * In this case, all the class files in the specified package, and any subpackages 193 * will be written to the jar file. 194 * </ul> 195 * 196 * @param fm the file manager in which to find the file objects 197 * @param l the location in which to find the file objects 198 * @param paths the paths specifying the file objects to be copied 199 * @return this task object 200 * @throws IOException if errors occur while determining the set of file objects 201 */ 202 public JarTask files(JavaFileManager fm, JavaFileManager.Location l, String... paths) 203 throws IOException { 204 for (String p : paths) { 205 if (p.endsWith(".**")) 206 addPackage(fm, l, p.substring(0, p.length() - 3), true); 207 else if (p.endsWith(".*")) 208 addPackage(fm, l, p.substring(0, p.length() - 2), false); 209 else 210 addFile(fm, l, p); 211 } 212 return this; 213 } 214 215 private void addPackage(JavaFileManager fm, JavaFileManager.Location l, String pkg, boolean recurse) 216 throws IOException { 217 for (JavaFileObject fo : fm.list(l, pkg, EnumSet.allOf(JavaFileObject.Kind.class), recurse)) { 218 fileObjects.add(fo); 219 } 220 } 221 222 private void addFile(JavaFileManager fm, JavaFileManager.Location l, String path) throws IOException { 223 JavaFileObject fo = fm.getJavaFileForInput(l, path, JavaFileObject.Kind.CLASS); 224 fileObjects.add(fo); 225 } 226 227 /** 228 * Provides limited jar command-like functionality. 229 * The supported commands are: 230 * <ul> 231 * <li> jar cf jarfile -C dir files... 232 * <li> jar cfm jarfile manifestfile -C dir files... 233 * </ul> 234 * Any values specified by other configuration methods will be ignored. 235 * @param args arguments in the style of those for the jar command 236 * @return a Result object containing the results of running the task 237 */ 238 public Task.Result run(String... args) { 239 if (args.length < 2) 240 throw new IllegalArgumentException(); 241 242 ListIterator<String> iter = Arrays.asList(args).listIterator(); 243 String first = iter.next(); 244 switch (first) { 245 case "cf": 246 jar = Paths.get(iter.next()); 247 break; 248 case "cfm": 249 jar = Paths.get(iter.next()); 250 try (InputStream in = Files.newInputStream(Paths.get(iter.next()))) { 251 manifest = new Manifest(in); 252 } catch (IOException e) { 253 throw new IOError(e); 254 } 255 break; 256 } 257 258 if (iter.hasNext()) { 259 if (iter.next().equals("-C")) 260 baseDir = Paths.get(iter.next()); 261 else 262 iter.previous(); 263 } 264 265 paths = new ArrayList<>(); 266 while (iter.hasNext()) 267 paths.add(Paths.get(iter.next())); 268 269 return run(); 270 } 271 272 /** 273 * {@inheritDoc} 274 * @return the name "jar" 275 */ 276 @Override 277 public String name() { 278 return "jar"; 279 } 280 281 /** 282 * Creates a jar file with the arguments as currently configured. 283 * @return a Result object indicating the outcome of the compilation 284 * and the content of any output written to stdout, stderr, or the 285 * main stream by the compiler. 286 * @throws TaskError if the outcome of the task is not as expected. 287 */ 288 @Override 289 public Task.Result run() { 290 Manifest m = (manifest == null) ? new Manifest() : manifest; 291 Attributes mainAttrs = m.getMainAttributes(); 292 if (mainClass != null) 293 mainAttrs.put(Attributes.Name.MAIN_CLASS, mainClass); 294 if (classpath != null) 295 mainAttrs.put(Attributes.Name.CLASS_PATH, classpath); 296 297 AbstractTask.StreamOutput sysOut = new AbstractTask.StreamOutput(System.out, System::setOut); 298 AbstractTask.StreamOutput sysErr = new AbstractTask.StreamOutput(System.err, System::setErr); 299 300 Map<Task.OutputKind, String> outputMap = new HashMap<>(); 301 302 try (OutputStream os = Files.newOutputStream(jar); 303 JarOutputStream jos = openJar(os, m)) { 304 writeFiles(jos); 305 writeFileObjects(jos); 306 } catch (IOException e) { 307 error("Exception while opening " + jar, e); 308 } finally { 309 outputMap.put(Task.OutputKind.STDOUT, sysOut.close()); 310 outputMap.put(Task.OutputKind.STDERR, sysErr.close()); 311 } 312 return checkExit(new Task.Result(toolBox, this, (errors == 0) ? 0 : 1, outputMap)); 313 } 314 315 private JarOutputStream openJar(OutputStream os, Manifest m) throws IOException { 316 if (m == null || m.getMainAttributes().isEmpty() && m.getEntries().isEmpty()) { 317 return new JarOutputStream(os); 318 } else { 319 if (m.getMainAttributes().get(Attributes.Name.MANIFEST_VERSION) == null) 320 m.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); 321 return new JarOutputStream(os, m); 322 } 323 } 324 325 private void writeFiles(JarOutputStream jos) throws IOException { 326 Path base = (baseDir == null) ? currDir : baseDir; 327 for (Path path : paths) { 328 Files.walkFileTree(base.resolve(path), new SimpleFileVisitor<Path>() { 329 @Override 330 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { 331 try { 332 String p = base.relativize(file) 333 .normalize() 334 .toString() 335 .replace(File.separatorChar, '/'); 336 JarEntry e = new JarEntry(p); 337 jos.putNextEntry(e); 338 try { 339 jos.write(Files.readAllBytes(file)); 340 } finally { 341 jos.closeEntry(); 342 } 343 return FileVisitResult.CONTINUE; 344 } catch (IOException e) { 345 error("Exception while adding " + file + " to jar file", e); 346 return FileVisitResult.TERMINATE; 347 } 348 } 349 }); 350 } 351 } 352 353 private void writeFileObjects(JarOutputStream jos) throws IOException { 354 for (FileObject fo : fileObjects) { 355 String p = guessPath(fo); 356 JarEntry e = new JarEntry(p); 357 jos.putNextEntry(e); 358 try { 359 byte[] buf = new byte[1024]; 360 try (BufferedInputStream in = new BufferedInputStream(fo.openInputStream())) { 361 int n; 362 while ((n = in.read(buf)) > 0) 363 jos.write(buf, 0, n); 364 } catch (IOException ex) { 365 error("Exception while adding " + fo.getName() + " to jar file", ex); 366 } 367 } finally { 368 jos.closeEntry(); 369 } 370 } 371 } 372 373 /* 374 * A jar: URL is of the form jar:URL!/<entry> where URL is a URL for the .jar file itself. 375 * In Symbol files (i.e. ct.sym) the underlying entry is prefixed META-INF/sym/<base>. 376 */ 377 private final Pattern jarEntry = Pattern.compile(".*!/(?:META-INF/sym/[^/]+/)?(.*)"); 378 379 /* 380 * A jrt: URL is of the form jrt:/<module>/<package>/<file> 381 */ 382 private final Pattern jrtEntry = Pattern.compile("/([^/]+)/(.*)"); 383 384 /* 385 * A file: URL is of the form file:/path/to/{modules,patches}/<module>/<package>/<file> 386 */ 387 private final Pattern fileEntry = Pattern.compile(".*/(?:modules|patches)/([^/]+)/(.*)"); 388 389 private String guessPath(FileObject fo) { 390 URI u = fo.toUri(); 391 switch (u.getScheme()) { 392 case "jar": { 393 Matcher m = jarEntry.matcher(u.getSchemeSpecificPart()); 394 if (m.matches()) { 395 return m.group(1); 396 } 397 break; 398 } 399 case "jrt": { 400 Matcher m = jrtEntry.matcher(u.getSchemeSpecificPart()); 401 if (m.matches()) { 402 return m.group(2); 403 } 404 break; 405 } 406 case "file": { 407 Matcher m = fileEntry.matcher(u.getSchemeSpecificPart()); 408 if (m.matches()) { 409 return m.group(2); 410 } 411 break; 412 } 413 } 414 throw new IllegalArgumentException(fo.getName() + "--" + fo.toUri()); 415 } 416 417 private void error(String message, Throwable t) { 418 toolBox.out.println("Error: " + message + ": " + t); 419 errors++; 420 } 421 422 private int errors; 423 }