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