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