langtools/test/tools/lib/toolbox/JarTask.java
changeset 36778 e04318f39f92
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/langtools/test/tools/lib/toolbox/JarTask.java	Thu Mar 31 15:20:50 2016 -0700
@@ -0,0 +1,423 @@
+/*
+ * Copyright (c) 2013, 2016, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package toolbox;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOError;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.jar.Attributes;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.tools.FileObject;
+import javax.tools.JavaFileManager;
+import javax.tools.JavaFileObject;
+import static toolbox.ToolBox.currDir;
+
+/**
+ * A task to configure and run the jar file utility.
+ */
+public class JarTask extends AbstractTask<JarTask> {
+    private Path jar;
+    private Manifest manifest;
+    private String classpath;
+    private String mainClass;
+    private Path baseDir;
+    private List<Path> paths;
+    private Set<FileObject> fileObjects;
+
+    /**
+     * Creates a task to write jar files, using API mode.
+     * @param toolBox the {@code ToolBox} to use
+     */
+    public JarTask(ToolBox toolBox) {
+        super(toolBox, Task.Mode.API);
+        paths = Collections.emptyList();
+        fileObjects = new LinkedHashSet<>();
+    }
+
+    /**
+     * Creates a JarTask for use with a given jar file.
+     * @param toolBox the {@code ToolBox} to use
+     * @param path the file
+     */
+    public JarTask(ToolBox toolBox, String path) {
+        this(toolBox);
+        jar = Paths.get(path);
+    }
+
+    /**
+     * Creates a JarTask for use with a given jar file.
+     * @param toolBox the {@code ToolBox} to use
+     * @param path the file
+     */
+    public JarTask(ToolBox toolBox, Path path) {
+        this(toolBox);
+        jar = path;
+    }
+
+    /**
+     * Sets a manifest for the jar file.
+     * @param manifest the manifest
+     * @return this task object
+     */
+    public JarTask manifest(Manifest manifest) {
+        this.manifest = manifest;
+        return this;
+    }
+
+    /**
+     * Sets a manifest for the jar file.
+     * @param manifest a string containing the contents of the manifest
+     * @return this task object
+     * @throws IOException if there is a problem creating the manifest
+     */
+    public JarTask manifest(String manifest) throws IOException {
+        this.manifest = new Manifest(new ByteArrayInputStream(manifest.getBytes()));
+        return this;
+    }
+
+    /**
+     * Sets the classpath to be written to the {@code Class-Path}
+     * entry in the manifest.
+     * @param classpath the classpath
+     * @return this task object
+     */
+    public JarTask classpath(String classpath) {
+        this.classpath = classpath;
+        return this;
+    }
+
+    /**
+     * Sets the class to be written to the {@code Main-Class}
+     * entry in the manifest..
+     * @param mainClass the name of the main class
+     * @return this task object
+     */
+    public JarTask mainClass(String mainClass) {
+        this.mainClass = mainClass;
+        return this;
+    }
+
+    /**
+     * Sets the base directory for files to be written into the jar file.
+     * @param baseDir the base directory
+     * @return this task object
+     */
+    public JarTask baseDir(String baseDir) {
+        this.baseDir = Paths.get(baseDir);
+        return this;
+    }
+
+    /**
+     * Sets the base directory for files to be written into the jar file.
+     * @param baseDir the base directory
+     * @return this task object
+     */
+    public JarTask baseDir(Path baseDir) {
+        this.baseDir = baseDir;
+        return this;
+    }
+
+    /**
+     * Sets the files to be written into the jar file.
+     * @param files the files
+     * @return this task object
+     */
+    public JarTask files(String... files) {
+        this.paths = Stream.of(files)
+                .map(file -> Paths.get(file))
+                .collect(Collectors.toList());
+        return this;
+    }
+
+    /**
+     * Adds a set of file objects to be written into the jar file, by copying them
+     * from a Location in a JavaFileManager.
+     * The file objects to be written are specified by a series of paths;
+     * each path can be in one of the following forms:
+     * <ul>
+     * <li>The name of a class. For example, java.lang.Object.
+     * In this case, the corresponding .class file will be written to the jar file.
+     * <li>the name of a package followed by {@code .*}. For example, {@code java.lang.*}.
+     * In this case, all the class files in the specified package will be written to
+     * the jar file.
+     * <li>the name of a package followed by {@code .**}. For example, {@code java.lang.**}.
+     * In this case, all the class files in the specified package, and any subpackages
+     * will be written to the jar file.
+     * </ul>
+     *
+     * @param fm the file manager in which to find the file objects
+     * @param l  the location in which to find the file objects
+     * @param paths the paths specifying the file objects to be copied
+     * @return this task object
+     * @throws IOException if errors occur while determining the set of file objects
+     */
+    public JarTask files(JavaFileManager fm, JavaFileManager.Location l, String... paths)
+            throws IOException {
+        for (String p : paths) {
+            if (p.endsWith(".**"))
+                addPackage(fm, l, p.substring(0, p.length() - 3), true);
+            else if (p.endsWith(".*"))
+                addPackage(fm, l, p.substring(0, p.length() - 2), false);
+            else
+                addFile(fm, l, p);
+        }
+        return this;
+    }
+
+    private void addPackage(JavaFileManager fm, JavaFileManager.Location l, String pkg, boolean recurse)
+            throws IOException {
+        for (JavaFileObject fo : fm.list(l, pkg, EnumSet.allOf(JavaFileObject.Kind.class), recurse)) {
+            fileObjects.add(fo);
+        }
+    }
+
+    private void addFile(JavaFileManager fm, JavaFileManager.Location l, String path) throws IOException {
+        JavaFileObject fo = fm.getJavaFileForInput(l, path, JavaFileObject.Kind.CLASS);
+        fileObjects.add(fo);
+    }
+
+    /**
+     * Provides limited jar command-like functionality.
+     * The supported commands are:
+     * <ul>
+     * <li> jar cf jarfile -C dir files...
+     * <li> jar cfm jarfile manifestfile -C dir files...
+     * </ul>
+     * Any values specified by other configuration methods will be ignored.
+     * @param args arguments in the style of those for the jar command
+     * @return a Result object containing the results of running the task
+     */
+    public Task.Result run(String... args) {
+        if (args.length < 2)
+            throw new IllegalArgumentException();
+
+        ListIterator<String> iter = Arrays.asList(args).listIterator();
+        String first = iter.next();
+        switch (first) {
+            case "cf":
+                jar = Paths.get(iter.next());
+                break;
+            case "cfm":
+                jar = Paths.get(iter.next());
+                try (InputStream in = Files.newInputStream(Paths.get(iter.next()))) {
+                    manifest = new Manifest(in);
+                } catch (IOException e) {
+                    throw new IOError(e);
+                }
+                break;
+        }
+
+        if (iter.hasNext()) {
+            if (iter.next().equals("-C"))
+                baseDir = Paths.get(iter.next());
+            else
+                iter.previous();
+        }
+
+        paths = new ArrayList<>();
+        while (iter.hasNext())
+            paths.add(Paths.get(iter.next()));
+
+        return run();
+    }
+
+    /**
+     * {@inheritDoc}
+     * @return the name "jar"
+     */
+    @Override
+    public String name() {
+        return "jar";
+    }
+
+    /**
+     * Creates a jar file with the arguments as currently configured.
+     * @return a Result object indicating the outcome of the compilation
+     * and the content of any output written to stdout, stderr, or the
+     * main stream by the compiler.
+     * @throws TaskError if the outcome of the task is not as expected.
+     */
+    @Override
+    public Task.Result run() {
+        Manifest m = (manifest == null) ? new Manifest() : manifest;
+        Attributes mainAttrs = m.getMainAttributes();
+        if (mainClass != null)
+            mainAttrs.put(Attributes.Name.MAIN_CLASS, mainClass);
+        if (classpath != null)
+            mainAttrs.put(Attributes.Name.CLASS_PATH, classpath);
+
+        AbstractTask.StreamOutput sysOut = new AbstractTask.StreamOutput(System.out, System::setOut);
+        AbstractTask.StreamOutput sysErr = new AbstractTask.StreamOutput(System.err, System::setErr);
+
+        Map<Task.OutputKind, String> outputMap = new HashMap<>();
+
+        try (OutputStream os = Files.newOutputStream(jar);
+                JarOutputStream jos = openJar(os, m)) {
+            writeFiles(jos);
+            writeFileObjects(jos);
+        } catch (IOException e) {
+            error("Exception while opening " + jar, e);
+        } finally {
+            outputMap.put(Task.OutputKind.STDOUT, sysOut.close());
+            outputMap.put(Task.OutputKind.STDERR, sysErr.close());
+        }
+        return checkExit(new Task.Result(toolBox, this, (errors == 0) ? 0 : 1, outputMap));
+    }
+
+    private JarOutputStream openJar(OutputStream os, Manifest m) throws IOException {
+        if (m == null || m.getMainAttributes().isEmpty() && m.getEntries().isEmpty()) {
+            return new JarOutputStream(os);
+        } else {
+            if (m.getMainAttributes().get(Attributes.Name.MANIFEST_VERSION) == null)
+                m.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
+            return new JarOutputStream(os, m);
+        }
+    }
+
+    private void writeFiles(JarOutputStream jos) throws IOException {
+            Path base = (baseDir == null) ? currDir : baseDir;
+            for (Path path : paths) {
+                Files.walkFileTree(base.resolve(path), new SimpleFileVisitor<Path>() {
+                    @Override
+                    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
+                        try {
+                        String p = base.relativize(file)
+                                .normalize()
+                                .toString()
+                                .replace(File.separatorChar, '/');
+                        JarEntry e = new JarEntry(p);
+                            jos.putNextEntry(e);
+                        try {
+                            jos.write(Files.readAllBytes(file));
+                        } finally {
+                            jos.closeEntry();
+                        }
+                            return FileVisitResult.CONTINUE;
+                        } catch (IOException e) {
+                        error("Exception while adding " + file + " to jar file", e);
+                            return FileVisitResult.TERMINATE;
+                        }
+                    }
+                });
+            }
+    }
+
+    private void writeFileObjects(JarOutputStream jos) throws IOException {
+        for (FileObject fo : fileObjects) {
+            String p = guessPath(fo);
+            JarEntry e = new JarEntry(p);
+            jos.putNextEntry(e);
+            try {
+                byte[] buf = new byte[1024];
+                try (BufferedInputStream in = new BufferedInputStream(fo.openInputStream())) {
+                    int n;
+                    while ((n = in.read(buf)) > 0)
+                        jos.write(buf, 0, n);
+                } catch (IOException ex) {
+                    error("Exception while adding " + fo.getName() + " to jar file", ex);
+                }
+        } finally {
+                jos.closeEntry();
+        }
+        }
+    }
+
+    /*
+     * A jar: URL is of the form  jar:URL!/<entry>  where URL is a URL for the .jar file itself.
+     * In Symbol files (i.e. ct.sym) the underlying entry is prefixed META-INF/sym/<base>.
+     */
+    private final Pattern jarEntry = Pattern.compile(".*!/(?:META-INF/sym/[^/]+/)?(.*)");
+
+    /*
+     * A jrt: URL is of the form  jrt:/modules/<module>/<package>/<file>
+     */
+    private final Pattern jrtEntry = Pattern.compile("/modules/([^/]+)/(.*)");
+
+    /*
+     * A file: URL is of the form  file:/path/to/{modules,patches}/<module>/<package>/<file>
+     */
+    private final Pattern fileEntry = Pattern.compile(".*/(?:modules|patches)/([^/]+)/(.*)");
+
+    private String guessPath(FileObject fo) {
+        URI u = fo.toUri();
+        switch (u.getScheme()) {
+            case "jar": {
+                Matcher m = jarEntry.matcher(u.getSchemeSpecificPart());
+                if (m.matches()) {
+                    return m.group(1);
+                }
+                break;
+            }
+            case "jrt": {
+                Matcher m = jrtEntry.matcher(u.getSchemeSpecificPart());
+                if (m.matches()) {
+                    return m.group(2);
+                }
+                break;
+            }
+            case "file": {
+                Matcher m = fileEntry.matcher(u.getSchemeSpecificPart());
+                if (m.matches()) {
+                    return m.group(2);
+                }
+                break;
+            }
+        }
+        throw new IllegalArgumentException(fo.getName() + "--" + fo.toUri());
+    }
+
+    private void error(String message, Throwable t) {
+        toolBox.out.println("Error: " + message + ": " + t);
+        errors++;
+    }
+
+    private int errors;
+}