| /* | |
| * 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.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(ToolBox.fileSeparatorChar, '/'); | |
| 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:/<module>/<package>/<file> | |
| */ | |
| private final Pattern jrtEntry = Pattern.compile("/([^/]+)/(.*)"); | |
| /* | |
| * 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; | |
| } |