/* * 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.dev.resource.impl.PathPrefix.Judgement; import com.google.gwt.dev.util.StringInterner; import com.google.gwt.dev.util.collect.Maps; import com.google.gwt.thirdparty.guava.common.collect.Lists; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; /** * Combines the information conveyed about a set of path prefixes to quickly * answer questions regarding an entire set of path prefixes. * <p> * Is effectively immutable and should not be modified after initial use. */ public class PathPrefixSet { /* * (1) TODO(amitmanjhi): Improve the api of the PathPrefixSet so that with one * trie-traversal, it could be found out which resources rooted at a directory * are allowed? */ private class TrieNode { // TODO(amitmanjhi): Consider the memory-speed tradeoff here private Map<String, TrieNode> children = Maps.create(); private final String part; private List<PathPrefix> prefixes = Lists.newArrayList(); private boolean hasPrefixes = false; public TrieNode(String part) { this.part = StringInterner.get().intern(part); } public TrieNode addChild(String part) { part = StringInterner.get().intern(part); TrieNode newChild = new TrieNode(part); assert !children.containsKey(part); children = Maps.put(children, part, newChild); return newChild; } public void addPathPrefix(PathPrefix prefix) { hasPrefixes = true; if (mergePathPrefixes) { if (prefixes.isEmpty()) { prefixes.add(prefix); } else { prefixes.get(0).merge(prefix); } } else { prefixes.add(prefix); } } public TrieNode findChild(String part) { return children.get(part); } public List<PathPrefix> getPathPrefixes() { return prefixes; } @Override public String toString() { StringBuilder sb = new StringBuilder(); toString(sb, ""); return sb.toString(); } private void toString(StringBuilder sb, String indent) { if (sb.length() > 0) { sb.append('\n'); } sb.append(indent); sb.append(' '); sb.append(part); for (TrieNode child : children.values()) { child.toString(sb, indent + " "); } } } /** * Whether or not to merge prefixes that are added. Merged prefixes perform better during resource * scanning but at the cost of not being able to tell which module(s) are responsible for a * particular resource inclusion. */ private boolean mergePathPrefixes = true; /** * List of all path prefixes in priority order. */ private final List<PathPrefix> prefixes = new ArrayList<PathPrefix>(); private final TrieNode rootTrieNode = new TrieNode("/"); public PathPrefixSet() { this(true); } public PathPrefixSet(boolean mergePathPrefixes) { this.mergePathPrefixes = mergePathPrefixes; } /** * @param prefix the prefix to add * @return <code>true</code> if the prefix was not already in the set; * otherwise, it merged with one having the same prefix, which has * the effect of expanding the filter (the merge works as * <code>union(includes - skips) - union(excludes)</code>) */ public boolean add(PathPrefix prefix) { prefix.setPriority(prefixes.size()); prefixes.add(prefix); String pathPrefix = prefix.getPrefix(); /* * An empty prefix means we have no prefix requirement, but we do attach the * prefix to the root so that we can apply the filter. */ if ("".equals(pathPrefix)) { rootTrieNode.addPathPrefix(prefix); return false; } // TODO(bruce): consider not using split for speed String[] parts = pathPrefix.split("/"); TrieNode parentNode = rootTrieNode; boolean didAdd = false; for (String part : parts) { TrieNode childNode = parentNode.findChild(part); if (childNode != null) { // Follow existing branch. parentNode = childNode; } else { // Add a new branch. parentNode = parentNode.addChild(part); didAdd = true; } } assert (parentNode != null); parentNode.addPathPrefix(prefix); return didAdd; } public int getSize() { return prefixes.size(); } /** * Determines whether or not a directory might have resources that could be * included. The primary purpose of this method is to allow * {@link ClassPathEntry} subclasses to avoid descending into directory * hierarchies that could not possibly contain resources that would be * included by {@link #includesResource(String)} * * @param dirPath must be a valid abstract directory name (must not be an * empty string) * @return true if some PathPrefix allows the directory */ public boolean includesDirectory(String dirPath) { assertValidAbstractDirectoryPathName(dirPath); /* * There are four cases: * * (1) The empty string was specified as a prefix, which causes everything * to be included. * * (2) As we walk the parts of dirPath, we see a path prefix attached to one * of the trie nodes we encounter. This means that there was a specified * prefix that this dirPath falls underneath, so it is included. * * (3) dirPath is longer than the trie, but we never encounter a path prefix * as we walk the trie. This indicates that this directory doesn't fall into * any of the specified prefixes. * * (4) dirPath is not longer than the trie and stays on the trie the whole * time, which means it is included (since at least some longer prefix * includes it). */ if (rootTrieNode.hasPrefixes) { // Case (1). return true; } TrieNode parentNode = rootTrieNode; String[] parts = dirPath.split("/"); for (String part : parts) { assert (!"".equals(part)); TrieNode childNode = parentNode.findChild(part); if (childNode != null) { if (childNode.hasPrefixes) { // Case (2). return true; } // Haven't found a path prefix yet, so keep walking. parentNode = childNode; } else { // Case (3). return false; } } // Case (4). return true; } /** * Determines whether or not a given resource should be allowed by this path * prefix set and the corresponding filters. * * @param resourceAbstractPathName * @return matching <code>PathPrefix</code> if the resource matches some * specified prefix and any associated filters don't exclude it. * Otherwise, returns null. So it returns null if either no prefixes * match or the most specific prefix excludes the resource. */ public ResourceResolution includesResource(String resourceAbstractPathName) { String[] parts = resourceAbstractPathName.split("/"); return includesResource(resourceAbstractPathName, parts); } /** * Dives down the package hierarchy looking for the most specific * package that applies to this resource. The filter of the most specific * package is the final determiner of inclusion/exclusion, such that more * specific subpackages can override the filter settings on less specific * superpackages. */ public ResourceResolution includesResource(String resourceAbstractPathName, String[] parts) { assertValidAbstractResourcePathName(resourceAbstractPathName); ResourceResolution resourceResolution = new ResourceResolution(); TrieNode currentNode = rootTrieNode; List<PathPrefix> mostSpecificPrefixes = rootTrieNode.getPathPrefixes(); // Walk all but the last path part, which is assumed to be a file name. for (String part : parts) { assert (!"".equals(part)); TrieNode childNode = currentNode.findChild(part); if (childNode == null) { break; } // We found a more specific node. if (childNode.hasPrefixes) { List<PathPrefix> moreSpecificPrefixes = childNode.getPathPrefixes(); // If PathPrefix->Module associations are accurate because PathPrefixes haven't been merged. if (!mergePathPrefixes) { // Record the module name of every PathPrefix that would allow this // resource. This enables detailed dependency validity checking. for (PathPrefix candidatePrefix : moreSpecificPrefixes) { if (candidatePrefix.getJudgement( resourceAbstractPathName).isInclude()) { resourceResolution.addSourceModuleName( candidatePrefix.getModuleName()); } } } mostSpecificPrefixes = moreSpecificPrefixes; } currentNode = childNode; } PathPrefix chiefPrefix = null; Judgement chiefJudgement = null; for (PathPrefix candidatePrefix : mostSpecificPrefixes) { Judgement judgement = candidatePrefix.getJudgement( resourceAbstractPathName); // EXCLUSION_EXCLUDE > FILTER_INCLUDE > IMPLICIT_EXCLUDE if (chiefJudgement == null || judgement.getPriority() > chiefJudgement.getPriority()) { chiefPrefix = candidatePrefix; chiefJudgement = judgement; } } if (chiefPrefix == null || !chiefJudgement.isInclude()) { return null; } resourceResolution.setPathPrefix(chiefPrefix); return resourceResolution; } public boolean mergePathPrefixes() { return mergePathPrefixes; } @Override public String toString() { return rootTrieNode.toString(); } public Collection<PathPrefix> values() { return Collections.unmodifiableCollection(prefixes); } private void assertValidAbstractDirectoryPathName(String name) { assert (name != null); assert (!name.startsWith("/")); } private void assertValidAbstractResourcePathName(String name) { assert (name != null); assert (!"".equals(name)); assert (!name.startsWith("/") && !name.endsWith("/")); } }