1/*
2 * Copyright (c) 2015, 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
24import java.io.File;
25import java.io.IOException;
26import java.io.OutputStream;
27import java.nio.file.Files;
28import java.nio.file.Path;
29import java.nio.file.Paths;
30import java.nio.file.StandardCopyOption;
31import java.util.ArrayList;
32import java.util.Enumeration;
33import java.util.List;
34import java.util.Set;
35import java.util.jar.JarEntry;
36import java.util.jar.JarFile;
37import java.util.jar.JarOutputStream;
38import java.util.jar.Manifest;
39import java.util.stream.Collectors;
40import java.util.stream.Stream;
41
42/**
43 * This class consists exclusively of static utility methods that are useful
44 * for creating and manipulating JAR files.
45 */
46
47public final class JarUtils {
48    private JarUtils() { }
49
50    /**
51     * Creates a JAR file.
52     *
53     * Equivalent to {@code jar cfm <jarfile> <manifest> -C <dir> file...}
54     *
55     * The input files are resolved against the given directory. Any input
56     * files that are directories are processed recursively.
57     */
58    public static void createJarFile(Path jarfile, Manifest man, Path dir, Path... file)
59        throws IOException
60    {
61        // create the target directory
62        Path parent = jarfile.getParent();
63        if (parent != null)
64            Files.createDirectories(parent);
65
66        List<Path> entries = new ArrayList<>();
67        for (Path entry : file) {
68            Files.find(dir.resolve(entry), Integer.MAX_VALUE,
69                        (p, attrs) -> attrs.isRegularFile())
70                    .map(e -> dir.relativize(e))
71                    .forEach(entries::add);
72        }
73
74        try (OutputStream out = Files.newOutputStream(jarfile);
75             JarOutputStream jos = new JarOutputStream(out))
76        {
77            if (man != null) {
78                JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
79                jos.putNextEntry(je);
80                man.write(jos);
81                jos.closeEntry();
82            }
83
84            for (Path entry : entries) {
85                String name = toJarEntryName(entry);
86                jos.putNextEntry(new JarEntry(name));
87                Files.copy(dir.resolve(entry), jos);
88                jos.closeEntry();
89            }
90        }
91    }
92
93    /**
94     * Creates a JAR file.
95     *
96     * Equivalent to {@code jar cf <jarfile>  -C <dir> file...}
97     *
98     * The input files are resolved against the given directory. Any input
99     * files that are directories are processed recursively.
100     */
101    public static void createJarFile(Path jarfile, Path dir, Path... file)
102        throws IOException
103    {
104        createJarFile(jarfile, null, dir, file);
105    }
106
107    /**
108     * Creates a JAR file.
109     *
110     * Equivalent to {@code jar cf <jarfile> -C <dir> file...}
111     *
112     * The input files are resolved against the given directory. Any input
113     * files that are directories are processed recursively.
114     */
115    public static void createJarFile(Path jarfile, Path dir, String... input)
116        throws IOException
117    {
118        Path[] paths = Stream.of(input).map(Paths::get).toArray(Path[]::new);
119        createJarFile(jarfile, dir, paths);
120    }
121
122    /**
123     * Creates a JAR file from the contents of a directory.
124     *
125     * Equivalent to {@code jar cf <jarfile> -C <dir> .}
126     */
127    public static void createJarFile(Path jarfile, Path dir) throws IOException {
128        createJarFile(jarfile, dir, Paths.get("."));
129    }
130
131    /**
132     * Update a JAR file.
133     *
134     * Equivalent to {@code jar uf <jarfile> -C <dir> file...}
135     *
136     * The input files are resolved against the given directory. Any input
137     * files that are directories are processed recursively.
138     */
139    public static void updateJarFile(Path jarfile, Path dir, Path... file)
140        throws IOException
141    {
142        List<Path> entries = new ArrayList<>();
143        for (Path entry : file) {
144            Files.find(dir.resolve(entry), Integer.MAX_VALUE,
145                    (p, attrs) -> attrs.isRegularFile())
146                    .map(e -> dir.relativize(e))
147                    .forEach(entries::add);
148        }
149
150        Set<String> names = entries.stream()
151                .map(JarUtils::toJarEntryName)
152                .collect(Collectors.toSet());
153
154        Path tmpfile = Files.createTempFile("jar", "jar");
155
156        try (OutputStream out = Files.newOutputStream(tmpfile);
157             JarOutputStream jos = new JarOutputStream(out))
158        {
159            // copy existing entries from the original JAR file
160            try (JarFile jf = new JarFile(jarfile.toString())) {
161                Enumeration<JarEntry> jentries = jf.entries();
162                while (jentries.hasMoreElements()) {
163                    JarEntry jentry = jentries.nextElement();
164                    if (!names.contains(jentry.getName())) {
165                        jos.putNextEntry(jentry);
166                        jf.getInputStream(jentry).transferTo(jos);
167                    }
168                }
169            }
170
171            // add the new entries
172            for (Path entry : entries) {
173                String name = toJarEntryName(entry);
174                jos.putNextEntry(new JarEntry(name));
175                Files.copy(dir.resolve(entry), jos);
176            }
177        }
178
179        // replace the original JAR file
180        Files.move(tmpfile, jarfile, StandardCopyOption.REPLACE_EXISTING);
181    }
182
183    /**
184     * Update a JAR file.
185     *
186     * Equivalent to {@code jar uf <jarfile> -C <dir> .}
187     */
188    public static void updateJarFile(Path jarfile, Path dir) throws IOException {
189        updateJarFile(jarfile, dir, Paths.get("."));
190    }
191
192
193    /**
194     * Map a file path to the equivalent name in a JAR file
195     */
196    private static String toJarEntryName(Path file) {
197        Path normalized = file.normalize();
198        return normalized.subpath(0, normalized.getNameCount())  // drop root
199                .toString()
200                .replace(File.separatorChar, '/');
201    }
202}
203