blob: 0790601f6100f46d8b3ced598d8db0e9757b3045 [file] [log] [blame]
/*
* 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);
}
}