ClassPathOpener.java revision 4c656e4ec2f5c5036dc67fb4034c1e7ff7abf343
1/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.dx.cf.direct;
18
19import com.android.dex.util.FileUtils;
20import java.io.ByteArrayOutputStream;
21import java.io.File;
22import java.io.IOException;
23import java.io.InputStream;
24import java.util.ArrayList;
25import java.util.Arrays;
26import java.util.Collections;
27import java.util.Comparator;
28import java.util.zip.ZipEntry;
29import java.util.zip.ZipFile;
30
31/**
32 * Opens all the class files found in a class path element. Path elements
33 * can point to class files, {jar,zip,apk} files, or directories containing
34 * class files.
35 */
36public class ClassPathOpener {
37
38    /** {@code non-null;} pathname to start with */
39    private final String pathname;
40    /** {@code non-null;} callback interface */
41    private final Consumer consumer;
42    /**
43     * If true, sort such that classes appear before their inner
44     * classes and "package-info" occurs before all other classes in that
45     * package.
46     */
47    private final boolean sort;
48    private FileNameFilter filter;
49
50    /**
51     * Callback interface for {@code ClassOpener}.
52     */
53    public interface Consumer {
54
55        /**
56         * Provides the file name and byte array for a class path element.
57         *
58         * @param name {@code non-null;} filename of element. May not be a valid
59         * filesystem path.
60         *
61         * @param lastModified milliseconds since 1970-Jan-1 00:00:00 GMT
62         * @param bytes {@code non-null;} file data
63         * @return true on success. Result is or'd with all other results
64         * from {@code processFileBytes} and returned to the caller
65         * of {@code process()}.
66         */
67        boolean processFileBytes(String name, long lastModified, byte[] bytes);
68
69        /**
70         * Informs consumer that an exception occurred while processing
71         * this path element. Processing will continue if possible.
72         *
73         * @param ex {@code non-null;} exception
74         */
75        void onException(Exception ex);
76
77        /**
78         * Informs consumer that processing of an archive file has begun.
79         *
80         * @param file {@code non-null;} archive file being processed
81         */
82        void onProcessArchiveStart(File file);
83    }
84
85    /**
86     * Filter interface for {@code ClassOpener}.
87     */
88    public interface FileNameFilter {
89
90        boolean accept(String path);
91    }
92
93    /**
94     * An accept all filter.
95     */
96    public static final FileNameFilter acceptAll = new FileNameFilter() {
97
98        @Override
99        public boolean accept(String path) {
100            return true;
101        }
102    };
103
104    /**
105     * Constructs an instance.
106     *
107     * @param pathname {@code non-null;} path element to process
108     * @param sort if true, sort such that classes appear before their inner
109     * classes and "package-info" occurs before all other classes in that
110     * package.
111     * @param consumer {@code non-null;} callback interface
112     */
113    public ClassPathOpener(String pathname, boolean sort, Consumer consumer) {
114        this(pathname, sort, acceptAll, consumer);
115    }
116
117    /**
118     * Constructs an instance.
119     *
120     * @param pathname {@code non-null;} path element to process
121     * @param sort if true, sort such that classes appear before their inner
122     * classes and "package-info" occurs before all other classes in that
123     * package.
124     * @param consumer {@code non-null;} callback interface
125     */
126    public ClassPathOpener(String pathname, boolean sort, FileNameFilter filter,
127            Consumer consumer) {
128        this.pathname = pathname;
129        this.sort = sort;
130        this.consumer = consumer;
131        this.filter = filter;
132    }
133
134    /**
135     * Processes a path element.
136     *
137     * @return the OR of all return values
138     * from {@code Consumer.processFileBytes()}.
139     */
140    public boolean process() {
141        File file = new File(pathname);
142
143        return processOne(file, true);
144    }
145
146    /**
147     * Processes one file.
148     *
149     * @param file {@code non-null;} the file to process
150     * @param topLevel whether this is a top-level file (that is,
151     * specified directly on the commandline)
152     * @return whether any processing actually happened
153     */
154    private boolean processOne(File file, boolean topLevel) {
155        try {
156            if (file.isDirectory()) {
157                return processDirectory(file, topLevel);
158            }
159
160            String path = file.getPath();
161
162            if (path.endsWith(".zip") ||
163                    path.endsWith(".jar") ||
164                    path.endsWith(".apk")) {
165                return processArchive(file);
166            }
167            if (filter.accept(path)) {
168                byte[] bytes = FileUtils.readFile(file);
169                return consumer.processFileBytes(path, file.lastModified(), bytes);
170            } else {
171                return false;
172            }
173        } catch (Exception ex) {
174            consumer.onException(ex);
175            return false;
176        }
177    }
178
179    /**
180     * Sorts java class names such that outer classes preceed their inner
181     * classes and "package-info" preceeds all other classes in its package.
182     *
183     * @param a {@code non-null;} first class name
184     * @param b {@code non-null;} second class name
185     * @return {@code compareTo()}-style result
186     */
187    private static int compareClassNames(String a, String b) {
188        // Ensure inner classes sort second
189        a = a.replace('$','0');
190        b = b.replace('$','0');
191
192        /*
193         * Assuming "package-info" only occurs at the end, ensures package-info
194         * sorts first.
195         */
196        a = a.replace("package-info", "");
197        b = b.replace("package-info", "");
198
199        return a.compareTo(b);
200    }
201
202    /**
203     * Processes a directory recursively.
204     *
205     * @param dir {@code non-null;} file representing the directory
206     * @param topLevel whether this is a top-level directory (that is,
207     * specified directly on the commandline)
208     * @return whether any processing actually happened
209     */
210    private boolean processDirectory(File dir, boolean topLevel) {
211        if (topLevel) {
212            dir = new File(dir, ".");
213        }
214
215        File[] files = dir.listFiles();
216        int len = files.length;
217        boolean any = false;
218
219        if (sort) {
220            Arrays.sort(files, new Comparator<File>() {
221                public int compare(File a, File b) {
222                    return compareClassNames(a.getName(), b.getName());
223                }
224            });
225        }
226
227        for (int i = 0; i < len; i++) {
228            any |= processOne(files[i], false);
229        }
230
231        return any;
232    }
233
234    /**
235     * Processes the contents of an archive ({@code .zip},
236     * {@code .jar}, or {@code .apk}).
237     *
238     * @param file {@code non-null;} archive file to process
239     * @return whether any processing actually happened
240     * @throws IOException on i/o problem
241     */
242    private boolean processArchive(File file) throws IOException {
243        ZipFile zip = new ZipFile(file);
244        ByteArrayOutputStream baos = new ByteArrayOutputStream(40000);
245        byte[] buf = new byte[20000];
246        boolean any = false;
247
248        ArrayList<? extends java.util.zip.ZipEntry> entriesList
249                = Collections.list(zip.entries());
250
251        if (sort) {
252            Collections.sort(entriesList, new Comparator<ZipEntry>() {
253               public int compare (ZipEntry a, ZipEntry b) {
254                   return compareClassNames(a.getName(), b.getName());
255               }
256            });
257        }
258
259        consumer.onProcessArchiveStart(file);
260
261        for (ZipEntry one : entriesList) {
262            if (one.isDirectory()) {
263                continue;
264            }
265
266            String path = one.getName();
267            InputStream in = zip.getInputStream(one);
268
269            baos.reset();
270            for (;;) {
271                int amt = in.read(buf);
272                if (amt < 0) {
273                    break;
274                }
275
276                baos.write(buf, 0, amt);
277            }
278
279            in.close();
280
281            byte[] bytes = baos.toByteArray();
282            any |= consumer.processFileBytes(path, one.getTime(), bytes);
283        }
284
285        zip.close();
286        return any;
287    }
288}
289