1 /*
2  * Copyright (C) 2011 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 
17 package com.android.tradefed.util;
18 
19 import com.android.ddmlib.Log;
20 
21 import java.io.File;
22 import java.io.IOException;
23 import java.util.Enumeration;
24 import java.util.LinkedHashMap;
25 import java.util.LinkedHashSet;
26 import java.util.LinkedList;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Set;
30 import java.util.jar.JarFile;
31 import java.util.regex.Pattern;
32 import java.util.zip.ZipEntry;
33 
34 /**
35  * Finds entries on classpath.
36  *
37  * <p>Adapted from vogar.target.ClassPathScanner</p>
38  */
39 public class ClassPathScanner {
40 
41     private static final String LOG_TAG = "ClassPathScanner";
42     private String[] mClassPath;
43 
44     /**
45      * A filter for classpath entry paths
46      * <p/>
47      * Patterned after {@link java.io.FileFilter}
48      */
49     public static interface IClassPathFilter {
50         /**
51          * Tests whether or not the specified abstract pathname should be included in a class path
52          * entry list.
53          *
54          * @param pathName the relative path of the class path entry
55          */
accept(String pathName)56         boolean accept(String pathName);
57 
58         /**
59          * An optional converter for a class path entry path names.
60          *
61          * @param pathName the relative path of the class path entry, in format "foo/path/file.ext".
62          * @return the pathName converted into context specific format
63          */
64 
transform(String pathName)65         String transform(String pathName);
66     }
67 
68     /**
69      * A {@link IClassPathFilter} that filters and transforms java class names.
70      */
71     public static class ClassNameFilter implements IClassPathFilter {
72         private static final String DOT_CLASS = ".class";
73 
74         /**
75          * {@inheritDoc}
76          */
77         @Override
accept(String pathName)78         public boolean accept(String pathName) {
79             return pathName.endsWith(DOT_CLASS);
80         }
81 
82         /**
83          * {@inheritDoc}
84          */
85         @Override
transform(String pathName)86         public String transform(String pathName) {
87             String className = pathName.substring(0, pathName.length() - DOT_CLASS.length());
88             className = className.replace('/', '.');
89             return className;
90         }
91 
92     }
93 
94     /**
95      * A {@link ClassNameFilter} that rejects inner classes
96      */
97     public static class ExternalClassNameFilter extends  ClassNameFilter {
98         /**
99          * {@inheritDoc}
100          */
101         @Override
accept(String pathName)102         public boolean accept(String pathName) {
103             return super.accept(pathName) && !pathName.contains("$");
104         }
105     }
106 
ClassPathScanner()107     public ClassPathScanner() {
108         mClassPath = getClassPath();
109     }
110 
111     /**
112      * Gets the names of all entries contained in given jar file, that match given filter
113      *
114      * @throws IOException
115      */
getEntriesFromJar(File plainFile, IClassPathFilter filter)116     public Map<String, String> getEntriesFromJar(File plainFile, IClassPathFilter filter)
117             throws IOException {
118         Map<String, String> entryNames = new LinkedHashMap<>();
119         JarFile jarFile = new JarFile(plainFile);
120         for (Enumeration<? extends ZipEntry> e = jarFile.entries(); e.hasMoreElements(); ) {
121             String entryName = e.nextElement().getName();
122             if (filter.accept(entryName)) {
123                 entryNames.put(filter.transform(entryName), plainFile.getName());
124             }
125             entryName = null;
126         }
127         jarFile.close();
128         return entryNames;
129     }
130 
131     /**
132      * Gets the names of all entries contained in given class path directory, that match given
133      * filter
134      * @throws IOException
135      */
getEntriesFromDir(File classPathDir, IClassPathFilter filter)136     public Set<String> getEntriesFromDir(File classPathDir, IClassPathFilter filter)
137             throws IOException {
138         Set<String> entryNames = new LinkedHashSet<String>();
139         getEntriesFromDir(classPathDir, entryNames, new LinkedList<String>(), filter);
140         return entryNames;
141     }
142 
143     /**
144      * Recursively adds the names of all entries contained in given class path directory,
145      * that match given filter.
146      *
147      * @param dir the directory to scan
148      * @param entries the {@link Set} of class path entry names to add to
149      * @param rootPath the relative path of <var>dir</var> from class path element root
150      * @param filter the {@link IClassPathFilter} to use
151      * @throws IOException
152      */
getEntriesFromDir(File dir, Set<String> entries, List<String> rootPath, IClassPathFilter filter)153     private void getEntriesFromDir(File dir, Set<String> entries, List<String> rootPath,
154             IClassPathFilter filter) throws IOException {
155         File[] childFiles = dir.listFiles();
156         if (childFiles == null) {
157             Log.w(LOG_TAG, String.format("Directory %s in classPath is not readable, skipping",
158                     dir.getAbsolutePath()));
159             return;
160         }
161         for (File childFile : childFiles) {
162             if (childFile.isDirectory()) {
163                 rootPath.add(childFile.getName() + "/");
164                 getEntriesFromDir(childFile, entries, rootPath, filter);
165                 // pop off the path element for this directory
166                 rootPath.remove(rootPath.size() - 1);
167             } else if (childFile.isFile()) {
168                 // construct relative path of this file
169                 String classPathEntryName = constructPath(rootPath, childFile.getName());
170                 if (filter.accept(classPathEntryName)) {
171                     entries.add(filter.transform(classPathEntryName));
172                 }
173             } else {
174                 Log.d(LOG_TAG, String.format("file %s in classPath is not recognized, skipping",
175                         dir.getAbsolutePath()));
176             }
177         }
178     }
179 
180     /**
181      * Construct a relative class path path for the given class path file
182      *
183      * @param rootPath the root path in {@link List} form
184      * @param fileName the file name
185      * @return the relative classpath path
186      */
constructPath(List<String> rootPath, String fileName)187     private String constructPath(List<String> rootPath, String fileName) {
188         StringBuilder pathBuilder = new StringBuilder();
189         for (String element : rootPath) {
190             pathBuilder.append(element);
191         }
192         pathBuilder.append(fileName);
193         return pathBuilder.toString();
194     }
195 
196     /**
197      * Retrieves set of classpath entries that match given {@link IClassPathFilter}
198      */
getClassPathEntries(IClassPathFilter filter)199     public Set<String> getClassPathEntries(IClassPathFilter filter) {
200         Set<String> entryNames = new LinkedHashSet<String>();
201         for (String classPathElement : mClassPath) {
202             File classPathFile = new File(classPathElement);
203             try {
204                 if (classPathFile.isFile() && classPathElement.endsWith(".jar")) {
205                     entryNames.addAll(getEntriesFromJar(classPathFile, filter).keySet());
206                 } else if (classPathFile.isDirectory()) {
207                     entryNames.addAll(getEntriesFromDir(classPathFile, filter));
208                 } else {
209                     Log.w(LOG_TAG, String.format(
210                             "class path entry %s does not exist or is not recognized, skipping",
211                             classPathElement));
212                 }
213             } catch (IOException e) {
214                 Log.w(LOG_TAG, String.format("Failed to read class path entry %s. Reason: %s",
215                         classPathElement, e.toString()));
216             }
217         }
218         return entryNames;
219     }
220 
221     /**
222      * Retrieves set of classpath entries that match given {@link IClassPathFilter} and returns them
223      * with which JAR they come from. Used to validate origin of files.
224      */
getClassPathEntriesFromJar(IClassPathFilter filter)225     public Map<String, String> getClassPathEntriesFromJar(IClassPathFilter filter) {
226         Map<String, String> entryNames = new LinkedHashMap<>();
227         for (String classPathElement : mClassPath) {
228             File classPathFile = new File(classPathElement);
229             try {
230                 if (classPathFile.isFile() && classPathElement.endsWith(".jar")) {
231                     entryNames.putAll(getEntriesFromJar(classPathFile, filter));
232                 } else {
233                     Log.w(
234                             LOG_TAG,
235                             String.format(
236                                     "class path entry %s does not exist or is not recognized, skipping",
237                                     classPathElement));
238                 }
239             } catch (IOException e) {
240                 Log.w(
241                         LOG_TAG,
242                         String.format(
243                                 "Failed to read class path entry %s. Reason: %s",
244                                 classPathElement, e.toString()));
245             }
246         }
247         return entryNames;
248     }
249 
250     /**
251      * Gets the class path from the System Property "java.class.path" and splits
252      * it up into the individual elements.
253      */
getClassPath()254     public static String[] getClassPath() {
255         String classPath = System.getProperty("java.class.path");
256         return classPath.split(Pattern.quote(File.pathSeparator));
257     }
258 }
259