/* * 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; }