1/*
2 * Copyright (C) 2012 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.test.runner;
18
19import dalvik.system.DexFile;
20
21import java.io.IOException;
22import java.util.ArrayList;
23import java.util.Arrays;
24import java.util.Enumeration;
25import java.util.HashSet;
26import java.util.LinkedHashSet;
27import java.util.List;
28import java.util.Set;
29
30/**
31 * Finds class entries in apks.
32 * <p/>
33 * Adapted from tools/tradefederation/..ClassPathScanner
34 */
35class ClassPathScanner {
36
37    /**
38     * A filter for classpath entry paths
39     * <p/>
40     * Patterned after {@link java.io.FileFilter}
41     */
42    public static interface ClassNameFilter {
43        /**
44         * Tests whether or not the specified abstract pathname should be included in a class path
45         * entry list.
46         *
47         * @param pathName the relative path of the class path entry
48         */
49        boolean accept(String className);
50    }
51
52    /**
53     * A {@link ClassNameFilter} that accepts all class names.
54     */
55    public static class AcceptAllFilter implements ClassNameFilter {
56
57        /**
58         * {@inheritDoc}
59         */
60        @Override
61        public boolean accept(String className) {
62            return true;
63        }
64
65    }
66
67    /**
68     * A {@link ClassNameFilter} that chains one or more filters together
69     */
70    public static class ChainedClassNameFilter implements ClassNameFilter {
71        private final List<ClassNameFilter> mFilters = new ArrayList<ClassNameFilter>();
72
73        public void add(ClassNameFilter filter) {
74            mFilters.add(filter);
75        }
76
77        public void addAll(ClassNameFilter... filters) {
78            mFilters.addAll(Arrays.asList(filters));
79        }
80
81        /**
82         * {@inheritDoc}
83         */
84        @Override
85        public boolean accept(String className) {
86            for (ClassNameFilter filter : mFilters) {
87                if (!filter.accept(className)) {
88                    return false;
89                }
90            }
91            return true;
92        }
93    }
94
95    /**
96     * A {@link ClassNameFilter} that rejects inner classes.
97     */
98    public static class ExternalClassNameFilter implements ClassNameFilter {
99        /**
100         * {@inheritDoc}
101         */
102        @Override
103        public boolean accept(String pathName) {
104            return !pathName.contains("$");
105        }
106    }
107
108    /**
109     * A {@link ClassNameFilter} that only accepts package names within the given namespace.
110     */
111    public static class InclusivePackageNameFilter implements ClassNameFilter {
112
113        private final String mPkgName;
114
115        InclusivePackageNameFilter(String pkgName) {
116            if (!pkgName.endsWith(".")) {
117                mPkgName = String.format("%s.", pkgName);
118            } else {
119                mPkgName = pkgName;
120            }
121        }
122
123        /**
124         * {@inheritDoc}
125         */
126        @Override
127        public boolean accept(String pathName) {
128            return pathName.startsWith(mPkgName);
129        }
130    }
131
132    /**
133     * A {@link ClassNameFilter} that only rejects a given package names within the given namespace.
134     */
135    public static class ExcludePackageNameFilter implements ClassNameFilter {
136
137        private final String mPkgName;
138
139        ExcludePackageNameFilter(String pkgName) {
140            if (!pkgName.endsWith(".")) {
141                mPkgName = String.format("%s.", pkgName);
142            } else {
143                mPkgName = pkgName;
144            }
145        }
146
147        /**
148         * {@inheritDoc}
149         */
150        @Override
151        public boolean accept(String pathName) {
152            return !pathName.startsWith(mPkgName);
153        }
154    }
155
156    private Set<String> mApkPaths = new HashSet<String>();
157
158    public ClassPathScanner(String... apkPaths) {
159        for (String apkPath : apkPaths) {
160            mApkPaths.add(apkPath);
161        }
162    }
163
164    /**
165     * Gets the names of all entries contained in given apk file, that match given filter.
166     * @throws IOException
167     */
168    private void addEntriesFromApk(Set<String> entryNames, String apkPath, ClassNameFilter filter)
169            throws IOException {
170        DexFile dexFile = null;
171        try {
172            dexFile = new DexFile(apkPath);
173            Enumeration<String> apkClassNames = getDexEntries(dexFile);
174            while (apkClassNames.hasMoreElements()) {
175                String apkClassName = apkClassNames.nextElement();
176                if (filter.accept(apkClassName)) {
177                    entryNames.add(apkClassName);
178                }
179            }
180        } finally {
181            if (dexFile != null) {
182                dexFile.close();
183            }
184        }
185    }
186
187    /**
188     * Retrieves the entry names from given {@link DexFile}.
189     * <p/>
190     * Exposed for unit testing.
191     *
192     * @param dexFile
193     * @return {@link Enumeration} of {@link String}s
194     */
195    Enumeration<String> getDexEntries(DexFile dexFile) {
196        return dexFile.entries();
197    }
198
199    /**
200     * Retrieves set of classpath entries that match given {@link ClassNameFilter}.
201     * @throws IOException
202     */
203    public Set<String> getClassPathEntries(ClassNameFilter filter) throws IOException {
204        // use LinkedHashSet for predictable order
205        Set<String> entryNames = new LinkedHashSet<String>();
206        for (String apkPath : mApkPaths) {
207            addEntriesFromApk(entryNames, apkPath, filter);
208        }
209        return entryNames;
210    }
211}
212