JarTask.java revision 3314:97ec97671022
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
24package toolbox;
25
26import java.io.BufferedInputStream;
27import java.io.ByteArrayInputStream;
28import java.io.File;
29import java.io.IOError;
30import java.io.IOException;
31import java.io.InputStream;
32import java.io.OutputStream;
33import java.net.URI;
34import java.nio.file.FileVisitResult;
35import java.nio.file.Files;
36import java.nio.file.Path;
37import java.nio.file.Paths;
38import java.nio.file.SimpleFileVisitor;
39import java.nio.file.attribute.BasicFileAttributes;
40import java.util.ArrayList;
41import java.util.Arrays;
42import java.util.Collections;
43import java.util.EnumSet;
44import java.util.HashMap;
45import java.util.LinkedHashSet;
46import java.util.List;
47import java.util.ListIterator;
48import java.util.Map;
49import java.util.Set;
50import java.util.jar.Attributes;
51import java.util.jar.JarEntry;
52import java.util.jar.JarOutputStream;
53import java.util.jar.Manifest;
54import java.util.regex.Matcher;
55import java.util.regex.Pattern;
56import java.util.stream.Collectors;
57import java.util.stream.Stream;
58import javax.tools.FileObject;
59import javax.tools.JavaFileManager;
60import javax.tools.JavaFileObject;
61import static toolbox.ToolBox.currDir;
62
63/**
64 * A task to configure and run the jar file utility.
65 */
66public 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}
424