001: /*
002: * Copyright 2006 Google Inc.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005: * use this file except in compliance with the License. You may obtain a copy of
006: * the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013: * License for the specific language governing permissions and limitations under
014: * the License.
015: */
016: package com.google.gwt.dev.util;
017:
018: import com.google.gwt.core.ext.TreeLogger;
019:
020: import java.io.File;
021: import java.io.IOException;
022: import java.net.MalformedURLException;
023: import java.net.URI;
024: import java.net.URISyntaxException;
025: import java.net.URL;
026: import java.net.URLClassLoader;
027: import java.util.ArrayList;
028: import java.util.Arrays;
029: import java.util.Enumeration;
030: import java.util.HashMap;
031: import java.util.Iterator;
032: import java.util.List;
033: import java.util.Map;
034: import java.util.jar.JarEntry;
035: import java.util.jar.JarFile;
036:
037: /**
038: * Creates a FileOracle based on a set of logical packages combined with either
039: * a URLClassLoader. For each specified package, the ClassLoader is searched for
040: * instances of that package as a directory. The results of this operation are
041: * merged together into a single list of URLs whose order is determined by the
042: * order of URLs in the ClassLoader. The relative order of different logical
043: * packages originating from the same URL in the ClassLoader is undefined.
044: *
045: * Once the sorted list of URLs is resolved, each URL is recursively searched to
046: * index all of its files (optionally, that pass the given FileOracleFilter).
047: * The results of this indexing are used to create the output FileOracle. Once
048: * the FileOracle is created, its index is fixed and no longer depends on the
049: * underlying URLClassLoader or file system. However, URLs returned from the
050: * FileOracle may become invalid if the contents of the file system change.
051: *
052: * Presently, only URLs beginning with <code>file:</code> and
053: * <code>jar:file:</code> can be inspected to index children. Any other types
054: * of URLs will generate a warning. The set of children indexed by
055: * <code>jar:file:</code> type URLs is fixed at creation time, but the set of
056: * children from <code>file:</code> type URLs will dynamically query the
057: * underlying file system.
058: */
059: public class FileOracleFactory {
060:
061: /**
062: * Used to decide whether or not a resource name should be included in an
063: * enumeration.
064: */
065: public interface FileFilter {
066: boolean accept(String name);
067: }
068:
069: /**
070: * Implementation of a FileOracle as an ordered (based on class path) list of
071: * abstract names (relative to some root), each mapped to a concrete URL.
072: */
073: private static final class FileOracleImpl extends FileOracle {
074:
075: private final String[] logicalNames;
076:
077: private final Map logicalToPhysical;
078:
079: /**
080: * Creates a new FileOracle.
081: *
082: * @param logicalNames An ordered list of abstract path name strings.
083: * @param logicalToPhysical A map of every item in logicalNames onto a URL.
084: */
085: public FileOracleImpl(List logicalNames, Map logicalToPhysical) {
086: this .logicalNames = (String[]) logicalNames
087: .toArray(new String[logicalNames.size()]);
088: this .logicalToPhysical = new HashMap(logicalToPhysical);
089: }
090:
091: /*
092: * (non-Javadoc)
093: *
094: * @see com.google.gwt.dev.util.FileOracle#find(java.lang.String)
095: */
096: public URL find(String partialPath) {
097: return (URL) logicalToPhysical.get(partialPath);
098: }
099:
100: /*
101: * (non-Javadoc)
102: *
103: * @see com.google.gwt.dev.util.FileOracle#getAllFiles()
104: */
105: public String[] getAllFiles() {
106: return logicalNames;
107: }
108:
109: /*
110: * (non-Javadoc)
111: *
112: * @see com.google.gwt.dev.util.FileOracle#isEmpty()
113: */
114: public boolean isEmpty() {
115: return logicalNames.length == 0;
116: }
117: }
118:
119: /**
120: * Given a set of logical packages, finds every occurrence of each of those
121: * packages within cl, and then sorts them relative to each other based on
122: * classPathUrlList.
123: *
124: * @param logger Logs the process.
125: * @param cl Provides the underlying class path.
126: * @param packageSet The input set of logical packages to search for and sort.
127: * @param classPathUrlList The order in which to sort the results.
128: * @param sortedUrls An output list to which urls are appended.
129: * @param sortedPackages An output list to which logical packages are appended
130: * exactly corresponding to appends made to sortedUrls.
131: * @param recordPackages If false, only empty strings are appended to
132: * sortedPackages.
133: */
134: private static void addPackagesInSortedOrder(TreeLogger logger,
135: URLClassLoader cl, Map packageMap, List classPathUrlList,
136: List sortedUrls, List sortedPackages, List sortedFilters,
137: boolean recordPackages) {
138:
139: // Exhaustively find every package on the classpath in an unsorted fashion
140: //
141: List unsortedUrls = new ArrayList();
142: List unsortedPackages = new ArrayList();
143: List unsortedFilters = new ArrayList();
144: for (Iterator itPkg = packageMap.keySet().iterator(); itPkg
145: .hasNext();) {
146: String curPkg = (String) itPkg.next();
147: FileFilter curFilter = (FileFilter) packageMap.get(curPkg);
148: try {
149: Enumeration found = cl.findResources(curPkg);
150: if (!recordPackages) {
151: curPkg = "";
152: }
153: while (found.hasMoreElements()) {
154: URL match = (URL) found.nextElement();
155: unsortedUrls.add(match);
156: unsortedPackages.add(curPkg);
157: unsortedFilters.add(curFilter);
158: }
159: } catch (IOException e) {
160: logger.log(TreeLogger.WARN,
161: "Unexpected error searching classpath for "
162: + curPkg, e);
163: }
164: }
165:
166: /*
167: * Now sort the collected list by the proper class path order. This is an
168: * O(N*M) operation, but it should be okay for what we're doing
169: */
170:
171: // pre-convert the List of URL to String[] to speed up the inner loop below
172: int c = unsortedUrls.size();
173: String[] unsortedUrlStrings = new String[c];
174: for (int i = 0; i < c; ++i) {
175: unsortedUrlStrings[i] = unsortedUrls.get(i).toString();
176: // strip the jar prefix for text matching purposes
177: if (unsortedUrlStrings[i].startsWith("jar:")) {
178: unsortedUrlStrings[i] = unsortedUrlStrings[i]
179: .substring(4);
180: }
181: }
182:
183: // now sort the URLs based on classPathUrlList
184: for (Iterator itCp = classPathUrlList.iterator(); itCp
185: .hasNext();) {
186: URL curCpUrl = (URL) itCp.next();
187: String curUrlString = curCpUrl.toExternalForm();
188: // find all URLs that match this particular entry
189: for (int i = 0; i < c; ++i) {
190: if (unsortedUrlStrings[i].startsWith(curUrlString)) {
191: sortedUrls.add(unsortedUrls.get(i));
192: sortedPackages.add(unsortedPackages.get(i));
193: sortedFilters.add(unsortedFilters.get(i));
194: }
195: }
196: }
197: }
198:
199: /**
200: * Index all the children of a particular folder (recursively).
201: *
202: * @param logger Logs the process.
203: * @param filter If non-null, filters out which files get indexed.
204: * @param stripBaseLen The number of characters to strip from the beginning of
205: * every child's file path when computing the logical name.
206: * @param curDir The directory to index.
207: * @param logicalNames An output List of Children found under this URL.
208: * @param logicalToPhysical An output Map of Children found under this URL
209: * mapped to their concrete URLs.
210: */
211: private static void indexFolder(TreeLogger logger,
212: FileFilter filter, int stripBaseLen, File curDir,
213: List logicalNames, Map logicalToPhysical) {
214: File[] files = curDir.listFiles();
215: for (int i = 0; i < files.length; i++) {
216: File f = files[i];
217: if (f.exists()) {
218: if (f.isDirectory()) {
219: indexFolder(logger, filter, stripBaseLen, f,
220: logicalNames, logicalToPhysical);
221: } else if (f.isFile()) {
222: try {
223: String logicalName = f.getAbsolutePath()
224: .substring(stripBaseLen);
225: logicalName = logicalName.replace(
226: File.separatorChar, '/');
227: if (logicalToPhysical.containsKey(logicalName)) {
228: // this logical name is shadowed
229: logger.log(TreeLogger.DEBUG,
230: "Ignoring already-resolved "
231: + logicalName, null);
232: continue;
233: }
234: if (filter != null
235: && !filter.accept(logicalName)) {
236: // filtered out
237: logger.log(TreeLogger.SPAM, "Filtered out "
238: + logicalName, null);
239: continue;
240: }
241: URL physicalUrl = f.toURL();
242: logicalToPhysical.put(logicalName, physicalUrl);
243: logicalNames.add(logicalName);
244: logger.log(TreeLogger.TRACE, "Found "
245: + logicalName, null);
246: } catch (IOException e) {
247: logger.log(TreeLogger.WARN,
248: "Unexpected error resolving " + f, e);
249: }
250: }
251: }
252: }
253: }
254:
255: /**
256: * Index all the children in a particular folder of a jar.
257: *
258: * @param logger Logs the process.
259: * @param filter If non-null, filters out which files get indexed.
260: * @param jarUrl The URL of the containing jar file.
261: * @param jarFile The jarFile to index.
262: * @param basePath The sub tree within the jarFile to index.
263: * @param pkgBase If non-empty, causes the logical names of children to be
264: * shorter (rooting them higher in the tree).
265: * @param logicalNames An output List of Children found under this URL.
266: * @param logicalToPhysical An output Map of Children found under this URL
267: * mapped to their concrete URLs.
268: */
269: private static void indexJar(TreeLogger logger, FileFilter filter,
270: String jarUrl, JarFile jarFile, String basePath,
271: String pkgBase, List logicalNames, Map logicalToPhysical) {
272: int prefixCharsToStrip = basePath.length() - pkgBase.length();
273: for (Enumeration enumJar = jarFile.entries(); enumJar
274: .hasMoreElements();) {
275: JarEntry jarEntry = (JarEntry) enumJar.nextElement();
276: String jarEntryName = jarEntry.getName();
277: if (jarEntryName.startsWith(basePath)
278: && !jarEntry.isDirectory()) {
279: String logicalName = jarEntryName
280: .substring(prefixCharsToStrip);
281: String physicalUrlString = jarUrl + "!/" + jarEntryName;
282: if (logicalToPhysical.containsKey(logicalName)) {
283: // this logical name is shadowed
284: logger.log(TreeLogger.DEBUG,
285: "Ignoring already-resolved " + logicalName,
286: null);
287: continue;
288: }
289: if (filter != null && !filter.accept(logicalName)) {
290: // filtered out
291: logger.log(TreeLogger.SPAM, "Filtered out "
292: + logicalName, null);
293: continue;
294: }
295: try {
296: URL physicalUrl = new URL(physicalUrlString);
297: logicalToPhysical.put(logicalName, physicalUrl);
298: logicalNames.add(logicalName);
299: logger.log(TreeLogger.TRACE,
300: "Found " + logicalName, null);
301: } catch (MalformedURLException e) {
302: logger.log(TreeLogger.WARN,
303: "Unexpected error resolving "
304: + physicalUrlString, e);
305: }
306: }
307: }
308: }
309:
310: /**
311: * Finds all children of the specified URL and indexes them.
312: *
313: * @param logger Logs the process.
314: * @param filter If non-null, filters out which files get indexed.
315: * @param url The URL to index, must be <code>file:</code> or
316: * <code>jar:file:</code>
317: * @param pkgBase A prefix to exclude when indexing children.
318: * @param logicalNames An output List of Children found under this URL.
319: * @param logicalToPhysical An output Map of Children found under this URL
320: * mapped to their concrete URLs.
321: * @throws URISyntaxException if an unexpected error occurs.
322: * @throws IOException if an unexpected error occurs.
323: */
324: private static void indexURL(TreeLogger logger, FileFilter filter,
325: URL url, String pkgBase, List logicalNames,
326: Map logicalToPhysical) throws URISyntaxException,
327: IOException {
328:
329: String urlString = url.toString();
330: if (url.getProtocol().equals("file")) {
331: URI uri = new URI(urlString);
332: File f = new File(uri);
333: if (f.isDirectory()) {
334: int prefixCharsToStrip = f.getAbsolutePath().length()
335: + 1 - pkgBase.length();
336: indexFolder(logger, filter, prefixCharsToStrip, f,
337: logicalNames, logicalToPhysical);
338: } else {
339: // We can't handle files here, only directories. If this is a jar
340: // reference, the url must come in as a "jar:file:<stuff>!/[stuff/]".
341: // Fall through.
342: logger.log(TreeLogger.WARN, "Unexpected error, " + f
343: + " is neither a file nor a jar", null);
344: }
345: } else if (url.getProtocol().equals("jar")) {
346: String path = url.getPath();
347: int pos = path.indexOf('!');
348: if (pos >= 0) {
349: String jarPath = path.substring(0, pos);
350: String dirPath = path.substring(pos + 2);
351: URL jarURL = new URL(jarPath);
352: if (jarURL.getProtocol().equals("file")) {
353: URI jarURI = new URI(jarURL.toString());
354: File f = new File(jarURI);
355: JarFile jarFile = new JarFile(f);
356: // From each child, strip off the leading classpath portion when
357: // determining the logical name (sans the pkgBase name we want!)
358: //
359: indexJar(logger, filter, "jar" + ":" + jarPath,
360: jarFile, dirPath, pkgBase, logicalNames,
361: logicalToPhysical);
362: } else {
363: logger
364: .log(
365: TreeLogger.WARN,
366: "Unexpected error, jar at "
367: + jarURL
368: + " must be a file: type URL",
369: null);
370: }
371: } else {
372: throw new URISyntaxException(path,
373: "Cannot locate '!' separator");
374: }
375: } else {
376: logger.log(TreeLogger.WARN, "Unknown URL type for "
377: + urlString, null);
378: }
379: }
380:
381: /**
382: * The underlying classloader.
383: */
384: private final URLClassLoader classLoader;
385:
386: /**
387: * A map of packages indexed from the root of the class path onto their
388: * corresponding FileFilters.
389: */
390: private final Map packages = new HashMap();
391:
392: /**
393: * A map of packages that become their own roots (that is their children are
394: * indexed relative to them) onto their corresponding FileFilters.
395: */
396: private final Map rootPackages = new HashMap();
397:
398: /**
399: * Creates a FileOracleFactory with the default URLClassLoader.
400: */
401: public FileOracleFactory() {
402: this ((URLClassLoader) FileOracleFactory.class.getClassLoader());
403: }
404:
405: /**
406: * Creates a FileOracleFactory.
407: *
408: * @param classLoader The underlying class path to use.
409: */
410: public FileOracleFactory(URLClassLoader classLoader) {
411: this .classLoader = classLoader;
412: }
413:
414: /**
415: * Adds a logical package to the product FileOracle. All instances of this
416: * package that can be found in the underlying URLClassLoader will have their
417: * their children indexed, relative to the class path entry on which they are
418: * found.
419: *
420: * @param packageAsPath For example, "com/google/gwt/core/client".
421: */
422: public void addPackage(String packageAsPath, FileFilter filter) {
423: packageAsPath = ensureTrailingBackslash(packageAsPath);
424: packages.put(packageAsPath, filter);
425: }
426:
427: /**
428: * Adds a logical root package to the product FileOracle. All instances of
429: * this package that can be found in the underlying URLClassLoader will have
430: * their their children indexed, relative to their location within
431: * packageAsPath. All root packages trump all non-root packages when
432: * determining the final precedence order.
433: *
434: * @param packageAsPath For example, "com/google/gwt/core/client".
435: */
436: public void addRootPackage(String packageAsPath, FileFilter filter) {
437: packageAsPath = ensureTrailingBackslash(packageAsPath);
438: rootPackages.put(packageAsPath, filter);
439: }
440:
441: /**
442: * Creates the product FileOracle based on the logical packages previously
443: * added.
444: *
445: * @param logger Logs the process.
446: * @return a new FileOracle.
447: */
448: public FileOracle create(TreeLogger logger) {
449:
450: // get the full expanded URL class path for sorting purposes
451: //
452: List classPathUrls = new ArrayList();
453: for (ClassLoader curCL = classLoader; curCL != null; curCL = curCL
454: .getParent()) {
455: if (curCL instanceof URLClassLoader) {
456: URLClassLoader curURLCL = (URLClassLoader) curCL;
457: URL[] curURLs = curURLCL.getURLs();
458: classPathUrls.addAll(Arrays.asList(curURLs));
459: }
460: }
461:
462: /*
463: * Collect a sorted list of URLs corresponding to all of the logical
464: * packages mapped onto the
465: */
466:
467: // The list of
468: List urls = new ArrayList();
469: List pkgNames = new ArrayList();
470: List filters = new ArrayList();
471:
472: // don't record package names for root packages, they are rebased
473: addPackagesInSortedOrder(logger, classLoader, rootPackages,
474: classPathUrls, urls, pkgNames, filters, false);
475: // record package names for non-root packages
476: addPackagesInSortedOrder(logger, classLoader, packages,
477: classPathUrls, urls, pkgNames, filters, true);
478:
479: // We have a complete sorted list of mapped URLs with package prefixes
480:
481: // Setup data collectors
482: List logicalNames = new ArrayList();
483: Map logicalToPhysical = new HashMap();
484:
485: for (int i = 0, c = urls.size(); i < c; ++i) {
486: try {
487: URL url = (URL) urls.get(i);
488: String pkgName = (String) pkgNames.get(i);
489: FileFilter filter = (FileFilter) filters.get(i);
490: TreeLogger branch = logger.branch(TreeLogger.TRACE, url
491: .toString(), null);
492: indexURL(branch, filter, url, pkgName, logicalNames,
493: logicalToPhysical);
494: } catch (URISyntaxException e) {
495: logger.log(TreeLogger.WARN,
496: "Unexpected error searching " + urls.get(i), e);
497: } catch (IOException e) {
498: logger.log(TreeLogger.WARN,
499: "Unexpected error searching " + urls.get(i), e);
500: }
501: }
502:
503: return new FileOracleImpl(logicalNames, logicalToPhysical);
504: }
505:
506: /**
507: * Helper method to regularize packages.
508: *
509: * @param packageAsPath For exmaple, "com/google/gwt/core/client"
510: * @return For example, "com/google/gwt/core/client/"
511: */
512: private String ensureTrailingBackslash(String packageAsPath) {
513: if (packageAsPath.endsWith("/")) {
514: return packageAsPath;
515: } else {
516: return packageAsPath + "/";
517: }
518: }
519: }
|