langtools/test/tools/lib/toolbox/JarTask.java
changeset 36778 e04318f39f92
equal deleted inserted replaced
36777:28d33fb9097f 36778:e04318f39f92
       
     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:/modules/<module>/<package>/<file>
       
   381      */
       
   382     private final Pattern jrtEntry = Pattern.compile("/modules/([^/]+)/(.*)");
       
   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 }