1/*
2 * Copyright (c) 2000, 2013, 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.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26/*
27 * Read an input file which is output from a java -verbose run,
28 * combine with an argument list of files and directories, and
29 * write a list of items to be included in a jar file.
30 */
31package build.tools.jarreorder;
32
33import java.io.BufferedReader;
34import java.io.File;
35import java.io.FileNotFoundException;
36import java.io.FileReader;
37import java.io.IOException;
38import java.util.Collections;
39import java.util.HashSet;
40import java.io.PrintStream;
41import java.io.FileOutputStream;
42import java.util.ArrayList;
43import java.util.List;
44import java.util.Set;
45
46public class JarReorder {
47
48    // To deal with output
49    private PrintStream out;
50
51    private void usage() {
52        String help;
53        help =
54                "Usage:  jar JarReorder [-m] [-o <outputfile>] <order_list> <exclude_list> <file> ...\n"
55                + "   -m            activate module mode, where every direct sub\n"
56                + "                 directory of the current working directory\n"
57                + "                 will be assumed to be a separate source root\n"
58                + "   order_list    is a file containing names of files to load\n"
59                + "                 in order at the end of a jar file unless\n"
60                + "                 excluded in the exclude list.\n"
61                + "   exclude_list  is a file containing names of files/directories\n"
62                + "                 NOT to be included in a jar file.\n"
63                + "\n"
64                + "The order_list or exclude_list may be replaced by a \"-\" if no\n"
65                + "data is to be provided.\n"
66                + "\n"
67                + "   The remaining arguments are files or directories to be included\n"
68                + "   in a jar file, from which will be excluded those entries which\n"
69                + "   appear in the exclude list.\n";
70        System.err.println(help);
71    }
72
73
74    /*
75     * Create the file list to be included in a jar file, such that the
76     * list will appear in a specific order, and allowing certain
77     * files and directories to be excluded.
78     *
79     * Command path arguments are
80     *    - optional -m for module mode.
81     *    - optional -o outputfile
82     *    - name of a file containing a set of files to be included in a jar file.
83     *    - name of a file containing a set of files (or directories) to be
84     *      excluded from the jar file.
85     *    - names of files or directories to be searched for files to include
86     *      in the jar file.
87     */
88    public static void main(String[] args) {
89        JarReorder jr = new JarReorder();
90        jr.run(args);
91    }
92
93    private void run(String args[]) {
94
95        int arglen = args.length;
96        int argpos = 0;
97
98        boolean moduleMode = false;
99
100        if (arglen > 0) {
101            // Check for module mode
102            if (args[argpos].equals("-m")) {
103                moduleMode = true;
104                argpos++;
105                arglen--;
106            }
107            // Look for "-o outputfilename" option
108            if (arglen >= 2 && args[argpos].equals("-o")) {
109                try {
110                    out = new PrintStream(new FileOutputStream(args[argpos+1]));
111                } catch (FileNotFoundException e) {
112                    System.err.println("Error: " + e.getMessage());
113                    e.printStackTrace(System.err);
114                    System.exit(1);
115                }
116                argpos += 2;
117                arglen -= 2;
118            } else {
119                System.err.println("Error: Illegal arg count");
120                System.exit(1);
121            }
122        } else {
123            out = System.out;
124        }
125
126        // Should be 2 or more args left
127        if (arglen <= 2) {
128            usage();
129            System.exit(1);
130        }
131
132        // Read the ordered set of files to be included in rt.jar.
133        // Read the set of files/directories to be excluded from rt.jar.
134        String classListFile = args[argpos];
135        String excludeListFile = args[argpos + 1];
136        argpos += 2;
137        arglen -= 2;
138
139        // If we run module mode, this will contain the list of subdirs to use
140        // as source roots. Otherwise it will just contain the current working
141        // dir.
142        List<File> moduleDirs = findModuleDirs(moduleMode);
143
144        // Create 2 lists and a set of processed files
145        List<String> orderList = readListFromFile(classListFile, true, moduleDirs);
146        List<String> excludeList = readListFromFile(excludeListFile, false, moduleDirs);
147        Set<String> processed = new HashSet<String>();
148
149        // Create set of all files and directories excluded, then expand
150        //   that list completely
151        Set<String> excludeSet = new HashSet<String>(excludeList);
152        Set<String> allFilesExcluded = expand(null, excludeSet, processed);
153
154        // Indicate all these have been processed, orderList too, kept to end.
155        processed.addAll(orderList);
156
157        // The remaining arguments are names of files/directories to be included
158        // in the jar file.
159        Set<String> inputSet = new HashSet<String>();
160        for (int i = 0; i < arglen; ++i) {
161            String name = args[argpos + i];
162            for (File dir : moduleDirs) {
163                String cleanName = cleanPath(new File(dir, name));
164                if ( cleanName != null && cleanName.length() > 0 && !inputSet.contains(cleanName) ) {
165                    inputSet.add(cleanName);
166                }
167            }
168        }
169
170        // Expand file/directory input so we get a complete set (except ordered)
171        //   Should be everything not excluded and not in order list.
172        Set<String> allFilesIncluded = expand(null, inputSet, processed);
173
174        // Create simple sorted list so we can add ordered items at end.
175        List<String> allFiles = new ArrayList<String>(allFilesIncluded);
176        Collections.sort(allFiles);
177
178        // Now add the ordered set to the end of the list.
179        // Add in REVERSE ORDER, so that the first element is closest to
180        // the end (and the index).
181        for (int i = orderList.size() - 1; i >= 0; --i) {
182            String s = orderList.get(i);
183            if (allFilesExcluded.contains(s)) {
184                // Disable this warning until 8005688 is fixed
185                // System.err.println("Included order file " + s
186                //    + " is also excluded, skipping.");
187            } else if (new File(s).exists()) {
188                allFiles.add(s);
189            } else {
190                System.err.println("Included order file " + s
191                    + " missing, skipping.");
192            }
193        }
194
195        // Print final results.
196        for (String str : allFiles) {
197            // If running in module mode, each line must be prepended with a
198            // '-C dir ' which points to the source root where that file is
199            // found.
200            if (moduleMode) {
201                int firstPathSep = str.indexOf(File.separator);
202                String moduleDir;
203                if (firstPathSep < 0) {
204                    moduleDir = ".";
205                } else {
206                    moduleDir = str.substring(0, firstPathSep);
207                }
208                String filePath = str.substring(firstPathSep + 1);
209                out.println("-C " + moduleDir + " " + filePath);
210            } else {
211                out.println(str);
212            }
213        }
214        out.flush();
215        out.close();
216    }
217
218    /*
219     * Read a file containing a list of files and directories into a List.
220     */
221    private List<String> readListFromFile(String fileName,
222            boolean addClassSuffix, List<File> moduleDirs) {
223
224        BufferedReader br = null;
225        List<String> list = new ArrayList<String>();
226        // If you see "-" for the name, just assume nothing was provided.
227        if ("-".equals(fileName)) {
228            return list;
229        }
230        try {
231            br = new BufferedReader(new FileReader(fileName));
232            // Read the input file a path at a time. # in column 1 is a comment.
233            while (true) {
234                String path = br.readLine();
235                if (path == null) {
236                    break;
237                }
238                // Look for comments
239                path = path.trim();
240                if (path.length() == 0
241                        || path.charAt(0) == '#') {
242                    continue;
243                }
244                // Add trailing .class if necessary
245                if (addClassSuffix && !path.endsWith(".class")) {
246                    path = path + ".class";
247                }
248                // Look for file in each module source root
249                boolean pathFound = false;
250                for (File dir : moduleDirs) {
251                    File file = new File(dir, path);
252                    if (file.exists()) {
253                        pathFound = true;
254                        // Normalize the path
255                        String cleanPath = cleanPath(new File(dir, path));
256                        // Add to list
257                        if (cleanPath != null && cleanPath.length() > 0 && !list.contains(cleanPath)) {
258                            list.add(cleanPath);
259                        }
260                    }
261                }
262                if (!pathFound) {
263                    System.err.println("WARNING: Path does not exist as file or directory: " + path);
264                }
265            }
266            br.close();
267        } catch (FileNotFoundException e) {
268            System.err.println("Can't find file \"" + fileName + "\".");
269            System.exit(1);
270        } catch (IOException e) {
271            e.printStackTrace();
272            System.exit(2);
273        }
274        return list;
275    }
276
277    /*
278     * Expands inputSet (files or dirs) into full set of all files that
279     * can be found by recursively descending directories.
280     * @param dir root directory
281     * @param inputSet set of files or dirs to look into
282     * @param processed files or dirs already processed
283     * @return set of files
284     */
285    private Set<String> expand(File dir,
286            Set<String> inputSet,
287            Set<String> processed) {
288        Set<String> includedFiles = new HashSet<String>();
289        if (inputSet.isEmpty()) {
290            return includedFiles;
291        }
292        for (String name : inputSet) {
293            // Depending on start location
294            File f = (dir == null) ? new File(name)
295                    : new File(dir, name);
296            // Normalized path to use
297            String path = cleanPath(f);
298            if (path != null && path.length() > 0
299                    && !processed.contains(path)) {
300                if (f.isFile()) {
301                    // Not in the excludeList, add it to both lists
302                    includedFiles.add(path);
303                    processed.add(path);
304                } else if (f.isDirectory()) {
305                    // Add the directory entries
306                    String[] dirList = f.list();
307                    Set<String> dirInputSet = new HashSet<String>();
308                    for (String x : dirList) {
309                        dirInputSet.add(x);
310                    }
311                    // Process all entries in this directory
312                    Set<String> subList = expand(f, dirInputSet, processed);
313                    includedFiles.addAll(subList);
314                    processed.add(path);
315                }
316            }
317        }
318        return includedFiles;
319    }
320
321    /**
322     * Find all module sub directories to be used as source roots.
323     * @param moduleMode If true, assume sub directories are modules, otherwise
324     *                   just use current working directory.
325     * @return List of all found source roots
326     */
327    private List<File> findModuleDirs(boolean moduleMode) {
328        File cwd = new File(".");
329        List<File> moduleDirs = new ArrayList<File>();
330        if (moduleMode) {
331            for (File f : cwd.listFiles()) {
332                if (f.isDirectory()) {
333                    moduleDirs.add(f);
334                }
335            }
336        } else {
337            moduleDirs.add(cwd);
338        }
339        return moduleDirs;
340    }
341
342    private String cleanPath(File f) {
343        String path = f.getPath();
344        if (f.isFile()) {
345            path = cleanFilePath(path);
346        } else if (f.isDirectory()) {
347            path = cleanDirPath(path);
348        } else {
349            System.err.println("WARNING: Path does not exist as file or directory: " + path);
350            path = null;
351        }
352        return path;
353    }
354
355    private String cleanFilePath(String path) {
356        // Remove leading and trailing whitespace
357        path = path.trim();
358        // Make all / and \ chars one
359        if (File.separatorChar == '/') {
360            path = path.replace('\\', '/');
361        } else {
362            path = path.replace('/', '\\');
363        }
364        // Remove leading ./
365        while (path.startsWith("." + File.separator)) {
366            path = path.substring(2);
367        }
368        return path;
369    }
370
371    private String cleanDirPath(String path) {
372        path = cleanFilePath(path);
373        // Make sure it ends with a file separator
374        if (!path.endsWith(File.separator)) {
375            path = path + File.separator;
376        }
377        // Remove any /./ in the path.
378        if (path.endsWith(File.separator + "." + File.separator)) {
379            path = path.substring(0, path.length() - 2);
380        }
381        return path;
382    }
383
384}
385