com.google.gwt.dev.resource.impl.ResourceOracleImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gwt.dev.resource.impl.ResourceOracleImpl.java

Source

/*
 * 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.resource.Resource;
import com.google.gwt.dev.resource.ResourceOracle;
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 org.apache.commons.collections.map.AbstractReferenceMap;
import org.apache.commons.collections.map.ReferenceMap;

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.net.URLClassLoader;
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 {@link ResourceOracle}.
 */
public class ResourceOracleImpl implements ResourceOracle {

    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 ClassPathEntry getClassPathEntry() {
            return resource.getClassPathEntry();
        }

        @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 ResourceData implements Comparable<ResourceData> {
        public final PathPrefix pathPrefix;
        public final AbstractResource resource;

        public ResourceData(AbstractResource resource, PathPrefix pathPrefix) {
            this.resource = pathPrefix.shouldReroot() ? new RerootedResource(resource, pathPrefix) : resource;
            this.pathPrefix = pathPrefix;
        }

        @Override
        public int compareTo(ResourceData 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 ResourceData)) {
                return false;
            }
            ResourceData other = (ResourceData) 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 + "}";
        }
    }

    @SuppressWarnings("unchecked")
    private static final Map<ClassLoader, List<ClassPathEntry>> classPathCache = new ReferenceMap(
            AbstractReferenceMap.WEAK, AbstractReferenceMap.HARD);

    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.ENGLISH);
            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, Thread.currentThread().getContextClassLoader());
    }

    /**
     * Preinitializes the classpath for a given {@link ClassLoader}.
     */
    public static void preload(TreeLogger logger, ClassLoader classLoader) {
        Event resourceOracle = SpeedTracerLogger.start(CompilerEventType.RESOURCE_ORACLE, "phase", "preload");
        List<ClassPathEntry> entries = getAllClassPathEntries(logger, classLoader);
        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();
    }

    /**
     * Rescans the associated paths to recompute the available resources.
     * 
     * TODO(conroy,scottb): This synchronization could be improved upon to allow
     * disjoint sets of oracles to be refreshed simultaneously.
     * 
     * @param logger status and error details are written here
     * @param first At least one ResourceOracleImpl must be passed to refresh
     * @param rest Callers may optionally pass several oracles
     */
    public static synchronized void refresh(TreeLogger logger, ResourceOracleImpl first,
            ResourceOracleImpl... rest) {
        int len = 1 + rest.length;
        ResourceOracleImpl[] oracles = new ResourceOracleImpl[1 + rest.length];
        oracles[0] = first;
        System.arraycopy(rest, 0, oracles, 1, rest.length);

        Event resourceOracle = SpeedTracerLogger.start(CompilerEventType.RESOURCE_ORACLE, "phase", "refresh");
        TreeLogger refreshBranch = Messages.REFRESHING_RESOURCES.branch(logger, null);

        /*
         * Allocate fresh data structures in anticipation of needing to honor the
         * "new identity for the collections if anything changes" guarantee. Use a
         * LinkedHashMap because we do not want the order to change.
         */
        List<Map<String, ResourceData>> resourceDataMaps = new ArrayList<Map<String, ResourceData>>();

        List<PathPrefixSet> pathPrefixSets = new ArrayList<PathPrefixSet>();
        for (ResourceOracleImpl oracle : oracles) {
            if (!oracle.classPath.equals(oracles[0].classPath)) {
                throw new IllegalArgumentException("Refreshing multiple oracles with different classpaths");
            }
            resourceDataMaps.add(new LinkedHashMap<String, ResourceData>());
            pathPrefixSets.add(oracle.pathPrefixSet);
        }

        /*
         * Walk across path roots (i.e. classpath entries) in priority order. This
         * is a "reverse painter's algorithm", relying on being careful never to add
         * a resource that has already been added to the new map under construction
         * to create the effect that resources founder earlier on the classpath take
         * precedence.
         * 
         * Exceptions: super has priority over non-super; and if there are two super
         * resources with the same path, the one with the higher-priority path
         * prefix wins.
         */
        for (ClassPathEntry pathRoot : oracles[0].classPath) {
            TreeLogger branchForClassPathEntry = Messages.EXAMINING_PATH_ROOT.branch(refreshBranch,
                    pathRoot.getLocation(), null);

            List<Map<AbstractResource, PathPrefix>> resourceToPrefixMaps = pathRoot
                    .findApplicableResources(branchForClassPathEntry, pathPrefixSets);
            for (int i = 0; i < len; ++i) {
                Map<String, ResourceData> resourceDataMap = resourceDataMaps.get(i);
                Map<AbstractResource, PathPrefix> resourceToPrefixMap = resourceToPrefixMaps.get(i);
                for (Entry<AbstractResource, PathPrefix> entry : resourceToPrefixMap.entrySet()) {
                    ResourceData newCpeData = new ResourceData(entry.getKey(), entry.getValue());
                    String resourcePath = newCpeData.resource.getPath();
                    ResourceData oldCpeData = resourceDataMap.get(resourcePath);
                    // Old wins unless the new resource has higher priority.
                    if (oldCpeData == null || oldCpeData.compareTo(newCpeData) < 0) {
                        resourceDataMap.put(resourcePath, newCpeData);
                    } else {
                        Messages.IGNORING_SHADOWED_RESOURCE.log(branchForClassPathEntry, resourcePath, null);
                    }
                }
            }
        }

        for (int i = 0; i < len; ++i) {
            Map<String, ResourceData> resourceDataMap = resourceDataMaps.get(i);
            Map<String, Resource> externalMap = new HashMap<String, Resource>();
            Set<Resource> externalSet = new HashSet<Resource>();
            for (Entry<String, ResourceData> entry : resourceDataMap.entrySet()) {
                String path = entry.getKey();
                ResourceData data = entry.getValue();
                externalMap.put(path, data.resource);
                externalSet.add(data.resource);
            }

            // Update exposed collections with new (unmodifiable) data structures.
            oracles[i].exposedResources = Collections.unmodifiableSet(externalSet);
            oracles[i].exposedResourceMap = Collections.unmodifiableMap(externalMap);
            oracles[i].exposedPathNames = Collections.unmodifiableSet(externalMap.keySet());
        }

        resourceOracle.end();
    }

    private static void addAllClassPathEntries(TreeLogger logger, ClassLoader classLoader,
            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 (; classLoader != null; classLoader = classLoader.getParent()) {
            if (classLoader instanceof URLClassLoader) {
                URLClassLoader urlClassLoader = (URLClassLoader) classLoader;
                URL[] urls = urlClassLoader.getURLs();
                for (URL url : urls) {
                    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,
            ClassLoader classLoader) {
        List<ClassPathEntry> classPath = classPathCache.get(classLoader);
        if (classPath == null) {
            classPath = new ArrayList<ClassPathEntry>();
            addAllClassPathEntries(logger, classLoader, classPath);
            classPathCache.put(classLoader, classPath);
        }
        return classPath;
    }

    private final List<ClassPathEntry> classPath;

    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> classPath) {
        this.classPath = classPath;
    }

    /**
     * Constructs a {@link ResourceOracleImpl} from the thread's default
     * {@link ClassLoader}.
     */
    public ResourceOracleImpl(TreeLogger logger) {
        this(logger, Thread.currentThread().getContextClassLoader());
    }

    /**
     * Constructs a {@link ResourceOracleImpl} from a {@link ClassLoader}. The
     * specified {@link ClassLoader} and all of its parents which are instances of
     * {@link URLClassLoader} will have their class path entries added to this
     * instances underlying class path.
     */
    public ResourceOracleImpl(TreeLogger logger, ClassLoader classLoader) {
        this(getAllClassPathEntries(logger, classLoader));
    }

    @Override
    public void clear() {
        exposedPathNames = Collections.emptySet();
        exposedResourceMap = Collections.emptyMap();
        exposedResources = Collections.emptySet();
    }

    @Override
    public Set<String> getPathNames() {
        return exposedPathNames;
    }

    public PathPrefixSet getPathPrefixes() {
        return pathPrefixSet;
    }

    @Override
    public Map<String, Resource> getResourceMap() {
        return exposedResourceMap;
    }

    @Override
    public Set<Resource> getResources() {
        return exposedResources;
    }

    public void setPathPrefixes(PathPrefixSet pathPrefixSet) {
        this.pathPrefixSet = pathPrefixSet;
    }

    // @VisibleForTesting
    List<ClassPathEntry> getClassPath() {
        return classPath;
    }
}