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 }