| /* |
| * Copyright 2008 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| * use this file except in compliance with the License. You may obtain a copy of |
| * the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations under |
| * the License. |
| */ |
| package com.google.gwt.dev.resource.impl; |
| |
| import com.google.gwt.core.ext.TreeLogger; |
| import com.google.gwt.dev.cfg.ResourceLoader; |
| import com.google.gwt.dev.cfg.ResourceLoaders; |
| import com.google.gwt.dev.resource.Resource; |
| import com.google.gwt.dev.util.log.speedtracer.CompilerEventType; |
| import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger; |
| import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger.Event; |
| import com.google.gwt.dev.util.msg.Message0; |
| import com.google.gwt.dev.util.msg.Message1String; |
| import com.google.gwt.thirdparty.guava.common.collect.HashMultimap; |
| import com.google.gwt.thirdparty.guava.common.collect.MapMaker; |
| import com.google.gwt.thirdparty.guava.common.collect.SetMultimap; |
| import com.google.gwt.thirdparty.guava.common.collect.Sets; |
| import com.google.gwt.thirdparty.guava.common.io.Files; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.net.URL; |
| import java.security.AccessControlException; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| |
| /** |
| * The normal implementation of {@code ResourceOracle}. |
| */ |
| public class ResourceOracleImpl extends AbstractResourceOracle { |
| |
| private static class Messages { |
| static final Message1String EXAMINING_PATH_ROOT = new Message1String(TreeLogger.DEBUG, |
| "Searching for resources within $0"); |
| static final Message1String IGNORING_SHADOWED_RESOURCE = |
| new Message1String( |
| TreeLogger.DEBUG, |
| "Resource '$0' is being shadowed by another resource higher in the classpath having the same name; this one will not be used"); |
| static final Message0 REFRESHING_RESOURCES = new Message0(TreeLogger.TRACE, |
| "Refreshing resources"); |
| } |
| |
| /** |
| * Wrapper object around a resource to change its path when it is rerooted. |
| */ |
| private static class RerootedResource extends AbstractResource { |
| private final String path; |
| private final AbstractResource resource; |
| |
| public RerootedResource(AbstractResource resource, PathPrefix pathPrefix) { |
| this.path = pathPrefix.getRerootedPath(resource.getPath()); |
| this.resource = resource; |
| } |
| |
| @Override |
| public long getLastModified() { |
| return resource.getLastModified(); |
| } |
| |
| @Override |
| public String getLocation() { |
| return resource.getLocation(); |
| } |
| |
| @Override |
| public String getPath() { |
| return path; |
| } |
| |
| @Override |
| public String getPathPrefix() { |
| int fullPathLen = resource.getPath().length(); |
| return resource.getPath().substring(0, fullPathLen - path.length()); |
| } |
| |
| @Override |
| public InputStream openContents() throws IOException { |
| return resource.openContents(); |
| } |
| |
| @Override |
| public boolean wasRerooted() { |
| return true; |
| } |
| } |
| |
| private static class ResourceDescription implements Comparable<ResourceDescription> { |
| public final PathPrefix pathPrefix; |
| public final AbstractResource resource; |
| |
| public ResourceDescription(AbstractResource resource, PathPrefix pathPrefix) { |
| this.resource = |
| pathPrefix.shouldReroot() ? new RerootedResource(resource, pathPrefix) : resource; |
| this.pathPrefix = pathPrefix; |
| } |
| |
| public boolean isPreferredOver(ResourceDescription that) { |
| return this.compareTo(that) > 0; |
| } |
| |
| @Override |
| public int compareTo(ResourceDescription other) { |
| // Rerooted takes precedence over not rerooted. |
| if (this.resource.wasRerooted() != other.resource.wasRerooted()) { |
| return this.resource.wasRerooted() ? 1 : -1; |
| } |
| // Compare priorities of the path prefixes, high number == high priority. |
| return this.pathPrefix.getPriority() - other.pathPrefix.getPriority(); |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (!(o instanceof ResourceDescription)) { |
| return false; |
| } |
| ResourceDescription other = (ResourceDescription) o; |
| return this.pathPrefix.getPriority() == other.pathPrefix.getPriority() |
| && this.resource.wasRerooted() == other.resource.wasRerooted(); |
| } |
| |
| @Override |
| public int hashCode() { |
| return (pathPrefix.getPriority() << 1) + (resource.wasRerooted() ? 1 : 0); |
| } |
| |
| @Override |
| public String toString() { |
| return "{" + resource + "," + pathPrefix + "}"; |
| } |
| } |
| |
| private static final Map<ResourceLoader, List<ClassPathEntry>> classPathCache = |
| new MapMaker().weakKeys().makeMap(); |
| |
| /** |
| * A mapping from resource paths to the name of the module that |
| * created the PathPrefix (usually because of a <source> entry) that made |
| * the resource path live. |
| * <p> |
| * For example com/google/gwt/user/client/DOM.java was made live by the |
| * com.google.gwt.user.User module. |
| */ |
| private SetMultimap<String, String> sourceModulesByTypeSourceName = HashMultimap.create(); |
| |
| public static void clearCache() { |
| classPathCache.clear(); |
| } |
| |
| public static ClassPathEntry createEntryForUrl(TreeLogger logger, URL url) |
| throws URISyntaxException, IOException { |
| if (url.getProtocol().equals("file")) { |
| File f = new File(url.toURI()); |
| String lowerCaseFileName = f.getName().toLowerCase(Locale.ROOT); |
| if (f.isDirectory()) { |
| return new DirectoryClassPathEntry(f); |
| } else if (f.isFile() && lowerCaseFileName.endsWith(".jar")) { |
| return ZipFileClassPathEntry.get(f); |
| } else if (f.isFile() && lowerCaseFileName.endsWith(".zip")) { |
| return ZipFileClassPathEntry.get(f); |
| } else { |
| // It's a file ending in neither jar nor zip, speculatively try to |
| // open as jar/zip anyway. |
| try { |
| return ZipFileClassPathEntry.get(f); |
| } catch (Exception ignored) { |
| } |
| if (logger.isLoggable(TreeLogger.TRACE)) { |
| logger.log(TreeLogger.TRACE, "Unexpected entry in classpath; " + f |
| + " is neither a directory nor an archive (.jar or .zip)"); |
| } |
| return null; |
| } |
| } else { |
| logger.log(TreeLogger.WARN, "Unknown URL type for " + url, null); |
| return null; |
| } |
| } |
| |
| /** |
| * Preinitializes the classpath from the thread default {@link ClassLoader}. |
| */ |
| public static void preload(TreeLogger logger) { |
| preload(logger, ResourceLoaders.fromContextClassLoader()); |
| } |
| |
| /** |
| * Preinitializes the classpath for a given {@link ResourceLoader}. |
| */ |
| public static void preload(TreeLogger logger, ResourceLoader resources) { |
| Event resourceOracle = |
| SpeedTracerLogger.start(CompilerEventType.RESOURCE_ORACLE, "phase", "preload"); |
| List<ClassPathEntry> entries = getAllClassPathEntries(logger, resources); |
| for (ClassPathEntry entry : entries) { |
| // We only handle pre-indexing jars, the file system could change. |
| if (entry instanceof ZipFileClassPathEntry) { |
| ZipFileClassPathEntry zpe = (ZipFileClassPathEntry) entry; |
| zpe.index(logger); |
| } |
| } |
| resourceOracle.end(); |
| } |
| |
| /** |
| * Returns a mapping from resource paths to the set of names of modules that created PathPrefixes |
| * (usually because of a <source> entry) that made the resource path live. |
| * <p> |
| * For example com/google/gwt/user/client/DOM.java was made live by the com.google.gwt.user.User |
| * module. |
| */ |
| public SetMultimap<String, String> getSourceModulesByTypeSourceName() { |
| return sourceModulesByTypeSourceName; |
| } |
| |
| /** |
| * Scans the associated paths to recompute the available resources. |
| * |
| * @param logger status and error details are written here |
| */ |
| public synchronized void scanResources(TreeLogger logger) { |
| Event resourceOracle = |
| SpeedTracerLogger.start(CompilerEventType.RESOURCE_ORACLE, "phase", "refresh"); |
| TreeLogger refreshBranch = Messages.REFRESHING_RESOURCES.branch(logger, null); |
| |
| Map<String, ResourceDescription> resourceDescriptionsByPath = |
| new LinkedHashMap<String, ResourceDescription>(); |
| |
| for (ClassPathEntry classPathEntry : classPathEntries) { |
| TreeLogger branchForClassPathEntry = |
| Messages.EXAMINING_PATH_ROOT.branch(refreshBranch, classPathEntry.getLocation(), null); |
| |
| Map<AbstractResource, ResourceResolution> prefixesByResource = |
| classPathEntry.findApplicableResources(branchForClassPathEntry, pathPrefixSet); |
| for (Entry<AbstractResource, ResourceResolution> entry : prefixesByResource.entrySet()) { |
| AbstractResource resource = entry.getKey(); |
| ResourceResolution resourceResolution = entry.getValue(); |
| ResourceDescription resourceDescription = |
| new ResourceDescription(resource, resourceResolution.getPathPrefix()); |
| String resourcePath = resourceDescription.resource.getPath(); |
| maybeRecordTypeForModule(resourceResolution, resourcePath); |
| |
| // In case of collision. |
| if (resourceDescriptionsByPath.containsKey(resourcePath)) { |
| ResourceDescription oldResourceDescription = resourceDescriptionsByPath.get(resourcePath); |
| if (resourceDescription.isPreferredOver(oldResourceDescription)) { |
| resourceDescriptionsByPath.put(resourcePath, resourceDescription); |
| } else { |
| Messages.IGNORING_SHADOWED_RESOURCE.log(branchForClassPathEntry, resourcePath, null); |
| } |
| } else { |
| resourceDescriptionsByPath.put(resourcePath, resourceDescription); |
| } |
| } |
| } |
| |
| Map<String, Resource> resourcesByPath = new HashMap<String, Resource>(); |
| for (Entry<String, ResourceDescription> entry : resourceDescriptionsByPath.entrySet()) { |
| resourcesByPath.put(entry.getKey(), entry.getValue().resource); |
| } |
| |
| // Update exposed collections with new (unmodifiable) data structures. |
| exposedResources = Collections.unmodifiableSet(Sets.newHashSet(resourcesByPath.values())); |
| exposedResourceMap = Collections.unmodifiableMap(resourcesByPath); |
| exposedPathNames = Collections.unmodifiableSet(resourcesByPath.keySet()); |
| |
| resourceOracle.end(); |
| } |
| |
| private void maybeRecordTypeForModule(ResourceResolution resourceResolution, |
| String resourcePath) { |
| // If PathPrefix->Module associations are inaccurate because PathPrefixes have been merged. |
| if (pathPrefixSet.mergePathPrefixes()) { |
| // Then don't record any Type<->Module associations since they won't be accurate; |
| return; |
| } |
| |
| sourceModulesByTypeSourceName.putAll(asTypeSourceName(resourcePath), |
| resourceResolution.getSourceModuleNames()); |
| } |
| |
| private String asTypeSourceName(String resourcePath) { |
| return resourcePath.replace(".java", "").replace("/", "."); |
| } |
| |
| private static void addAllClassPathEntries(TreeLogger logger, ResourceLoader loader, |
| List<ClassPathEntry> classPath) { |
| // URL is expensive in collections, so we use URI instead |
| // See: |
| // http://michaelscharf.blogspot.com/2006/11/javaneturlequals-and-hashcode-make.html |
| Set<URI> seenEntries = new HashSet<URI>(); |
| |
| for (URL url : loader.getClassPath()) { |
| URI uri; |
| try { |
| uri = url.toURI(); |
| } catch (URISyntaxException e) { |
| logger.log(TreeLogger.WARN, "Error processing classpath URL '" + url + "'", e); |
| continue; |
| } |
| if (seenEntries.contains(uri)) { |
| continue; |
| } |
| seenEntries.add(uri); |
| Throwable caught; |
| try { |
| ClassPathEntry entry = createEntryForUrl(logger, url); |
| if (entry != null) { |
| classPath.add(entry); |
| } |
| continue; |
| } catch (AccessControlException e) { |
| if (logger.isLoggable(TreeLogger.DEBUG)) { |
| logger.log(TreeLogger.DEBUG, "Skipping URL due to access restrictions: " + url); |
| } |
| continue; |
| } catch (URISyntaxException e) { |
| caught = e; |
| } catch (IOException e) { |
| caught = e; |
| } |
| logger.log(TreeLogger.WARN, "Error processing classpath URL '" + url + "'", caught); |
| } |
| } |
| |
| private static synchronized List<ClassPathEntry> getAllClassPathEntries(TreeLogger logger, |
| ResourceLoader resources) { |
| List<ClassPathEntry> classPath = classPathCache.get(resources); |
| if (classPath == null) { |
| classPath = new ArrayList<ClassPathEntry>(); |
| addAllClassPathEntries(logger, resources, classPath); |
| classPathCache.put(resources, classPath); |
| } |
| return classPath; |
| } |
| |
| private final List<ClassPathEntry> classPathEntries; |
| |
| private Set<String> exposedPathNames = Collections.emptySet(); |
| |
| private Map<String, Resource> exposedResourceMap = Collections.emptyMap(); |
| |
| private Set<Resource> exposedResources = Collections.emptySet(); |
| |
| private PathPrefixSet pathPrefixSet = new PathPrefixSet(); |
| |
| /** |
| * Constructs a {@link ResourceOracleImpl} from a set of |
| * {@link ClassPathEntry ClassPathEntries}. The list is held by reference and |
| * must not be modified. |
| */ |
| public ResourceOracleImpl(List<ClassPathEntry> classPathEntries) { |
| this.classPathEntries = classPathEntries; |
| } |
| |
| /** |
| * Constructs a {@link ResourceOracleImpl} from the thread's default |
| * {@link ClassLoader}. |
| */ |
| public ResourceOracleImpl(TreeLogger logger) { |
| this(logger, ResourceLoaders.fromContextClassLoader()); |
| } |
| |
| public ResourceOracleImpl(TreeLogger logger, ResourceLoader resources) { |
| this(getAllClassPathEntries(logger, resources)); |
| } |
| |
| @Override |
| public void clear() { |
| sourceModulesByTypeSourceName.clear(); |
| exposedPathNames = Collections.emptySet(); |
| exposedResourceMap = Collections.emptyMap(); |
| exposedResources = Collections.emptySet(); |
| } |
| |
| @Override |
| public Set<String> getPathNames() { |
| return exposedPathNames; |
| } |
| |
| public PathPrefixSet getPathPrefixes() { |
| return pathPrefixSet; |
| } |
| |
| @Override |
| public Set<Resource> getResources() { |
| return exposedResources; |
| } |
| |
| public void setPathPrefixes(PathPrefixSet pathPrefixSet) { |
| this.pathPrefixSet = pathPrefixSet; |
| } |
| |
| // @VisibleForTesting |
| List<ClassPathEntry> getClassPathEntries() { |
| return classPathEntries; |
| } |
| |
| @Override |
| public Resource getResource(String pathName) { |
| pathName = Files.simplifyPath(pathName); |
| return exposedResourceMap.get(pathName); |
| } |
| } |