blob: f412f08c247f15d2b39c35efb94dcd725ad1de2b [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.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("/"));
}
}