| /* |
| * 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.util.StringInterner; |
| import com.google.gwt.dev.util.collect.Maps; |
| |
| 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. |
| */ |
| 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 static class TrieNode { |
| // TODO(amitmanjhi): Consider the memory-speed tradeoff here |
| private Map<String, TrieNode> children = Maps.create(); |
| private final String part; |
| |
| private PathPrefix prefix; |
| |
| 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 extendPathPrefix(PathPrefix prefix) { |
| if (this.prefix == null) { |
| this.prefix = prefix; |
| } else { |
| this.prefix.merge(prefix); |
| } |
| } |
| |
| public TrieNode findChild(String part) { |
| return children.get(part); |
| } |
| |
| public PathPrefix getPathPrefix() { |
| return prefix; |
| } |
| |
| public boolean hasChildren() { |
| return !children.isEmpty(); |
| } |
| |
| @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 + " "); |
| } |
| } |
| } |
| |
| /** |
| * List of all path prefixes in priority order. |
| */ |
| private final List<PathPrefix> prefixes = new ArrayList<PathPrefix>(); |
| |
| private final TrieNode rootTrieNode = new TrieNode("/"); |
| |
| /** |
| * @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.extendPathPrefix(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.extendPathPrefix(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.getPathPrefix() != null) { |
| // 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) { |
| PathPrefix pathPrefix = childNode.getPathPrefix(); |
| if (pathPrefix != null) { |
| // 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 PathPrefix includesResource(String resourceAbstractPathName) { |
| String[] parts = resourceAbstractPathName.split("/"); |
| return includesResource(resourceAbstractPathName, parts); |
| } |
| |
| /** |
| * Implementation of {@link #includesDirectory(String)}. |
| */ |
| public PathPrefix includesResource(String resourceAbstractPathName, String[] parts) { |
| /* |
| * Algorithm: dive 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. |
| */ |
| |
| assertValidAbstractResourcePathName(resourceAbstractPathName); |
| |
| TrieNode currentNode = rootTrieNode; |
| PathPrefix mostSpecificPrefix = rootTrieNode.getPathPrefix(); |
| |
| // 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) { |
| // We found a more specific node. |
| PathPrefix moreSpecificPrefix = childNode.getPathPrefix(); |
| if (moreSpecificPrefix != null) { |
| mostSpecificPrefix = moreSpecificPrefix; |
| } |
| currentNode = childNode; |
| } else { |
| // No valid branch to follow. |
| break; |
| } |
| } |
| |
| if (mostSpecificPrefix == null |
| || !mostSpecificPrefix.allows(resourceAbstractPathName)) { |
| /* |
| * Didn't match any specified prefix or the filter of the most specific |
| * prefix disallows the resource |
| */ |
| return null; |
| } |
| |
| return mostSpecificPrefix; |
| } |
| |
| @Override |
| public String toString() { |
| return rootTrieNode.toString(); |
| } |
| |
| public Collection<PathPrefix> values() { |
| return Collections.unmodifiableCollection(prefixes); |
| } |
| |
| private void assertValidAbstractDirectoryPathName(String name) { |
| assert (name != null); |
| // assert ("".equals(name) || (!name.startsWith("/") && |
| // name.endsWith("/"))); |
| assert (!name.startsWith("/") && name.endsWith("/")); |
| } |
| |
| private void assertValidAbstractResourcePathName(String name) { |
| assert (name != null); |
| assert (!"".equals(name)); |
| assert (!name.startsWith("/") && !name.endsWith("/")); |
| } |
| } |