001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.spi.project.support.ant;
043:
044: import java.io.File;
045: import java.util.Collections;
046: import java.util.HashSet;
047: import java.util.Iterator;
048: import java.util.Set;
049: import java.util.SortedSet;
050: import java.util.StringTokenizer;
051: import java.util.TreeSet;
052: import java.util.regex.Matcher;
053: import java.util.regex.Pattern;
054:
055: /**
056: * Utility to match Ant-style file patterns with extended glob syntax.
057: * <p>
058: * A path matcher can be given an optional list of include patterns,
059: * and an optional list of exclude patterns. A given file path
060: * matches the pattern if it is matched by at least one include
061: * pattern (or there is a null includes list), and is not matched by
062: * any of the exclude patterns (if this list is not null).
063: * </p>
064: * <p>
065: * The format is based on Ant patterns. Some details:
066: * </p>
067: * <ul>
068: * <li>A file path to be matched must be a <samp>/</samp>-separated
069: * relative path from an unspecified base directory. A path representing
070: * a folder must end in <samp>/</samp>, except for the path representing
071: * the root folder, which is the empty string. Thus, the full path to a file
072: * is always the simple concatenation of the path to its containing folder,
073: * and the file's basename.
074: * <li>An include or exclude list, if not null, is a list of nonempty patterns separated
075: * by spaces and/or commas. It may be an empty list; this is equivalent to null in the
076: * case of excludes, but in the case of includes means that nothing matches.
077: * <li>A pattern may use either <samp>/</samp> or <samp>\</samp> as a path separator
078: * interchangeably.
079: * <li>Most characters in a pattern match literally, and match a complete file path.
080: * A folder path ends in <samp>/</samp>, so the pattern <samp>foo</samp> will <em>not</em>
081: * match a folder named <samp>foo</samp>.
082: * <li><samp>*</samp> in a pattern matches zero or more characters within a path component
083: * (i.e. not including <samp>/</samp>).
084: * <li><samp>**</samp> matches zero or more complete path components. It must be preceded
085: * by a slash (or be at the beginning of the pattern) and be followed by a slash (or be at
086: * the end of the pattern).
087: * <li><samp>foo/</samp> is treated the same as <samp>foo/**</samp> and matches the whole
088: * tree rooted at the folder <samp>foo</samp>.
089: * <li><samp>/**<!---->/</samp> can match just a single <samp>/</samp>. <samp>**<!---->/</samp>
090: * and <samp>/**</samp> can match the empty string at path boundaries.
091: * </ul>
092: * <p>
093: * Some example patterns:
094: * </p>
095: * <dl>
096: * <dt><samp>foo/bar/</samp>
097: * <dd>The folder <samp>foo/bar</samp> and anything inside it.
098: * <dt><samp>foo/bar/baz</samp>
099: * <dd>The file <samp>foo/bar/baz</samp>.
100: * <dt><samp>**<!---->/foo/</samp>
101: * <dd>Any folder named <samp>foo</samp> and anything inside it.
102: * <dt><samp>**<!---->/*.java</samp>
103: * <dd>Any Java source file (even in the default package).
104: * </dl>
105: * @since org.netbeans.modules.project.ant/1 1.15
106: * @author Jesse Glick
107: */
108: public final class PathMatcher {
109:
110: private final String includes, excludes;
111: private final Pattern includePattern, excludePattern;
112: private final File base;
113: private final Set<String> knownIncludes;
114:
115: /**
116: * Create a path matching object.
117: * It is faster to create one matcher and call {@link #matches} multiple times
118: * than to recreate a matcher for each query.
119: * @param includes a list of paths to match, or null to match everything by default
120: * @param excludes a list of paths to not match, or null
121: * @param base a base directory to scan for known include roots (see {@link #findIncludedRoots}), or null if unknown
122: */
123: public PathMatcher(String includes, String excludes, File base) {
124: this .includes = includes;
125: this .excludes = excludes;
126: includePattern = computePattern(includes);
127: excludePattern = computePattern(excludes);
128: this .base = base;
129: knownIncludes = computeKnownIncludes();
130: }
131:
132: private Pattern computePattern(String patterns) {
133: if (patterns == null) {
134: return null;
135: }
136: StringBuilder rx = new StringBuilder();
137: StringTokenizer patternstok = new StringTokenizer(patterns,
138: ", "); // NOI18N
139: if (!patternstok.hasMoreTokens()) {
140: return Pattern.compile("<cannot match>"); // NOI18N
141: }
142: while (patternstok.hasMoreTokens()) {
143: String pattern = patternstok.nextToken().replace('\\', '/');
144: if (rx.length() > 0) {
145: rx.append('|');
146: }
147: if (pattern.endsWith("/")) { // NOI18N
148: pattern += "**"; // NOI18N
149: }
150: if (pattern.equals("**")) { // NOI18N
151: rx.append(".*"); // NOI18N
152: break;
153: }
154: Matcher m = Pattern.compile(
155: "/\\*\\*/|/\\*\\*|\\*\\*/|/\\*$|\\*|/|[^*/]+")
156: .matcher(pattern); // NOI18N
157: while (m.find()) {
158: String t = m.group();
159: if (t.equals("/**")) {
160: rx.append("/.*");
161: } else if (t.equals("**/")) {
162: rx.append("(.*/|)");
163: } else if (t.equals("/**/")) {
164: rx.append("(/.*/|/)");
165: } else if (t.equals("/*")) { // #98235
166: rx.append("/[^/]+");
167: } else if (t.equals("*")) {
168: rx.append("[^/]*");
169: } else {
170: rx.append(Pattern.quote(t));
171: }
172: }
173: }
174: String rxs = rx.toString();
175: return Pattern.compile(rxs);
176: }
177:
178: /**
179: * Check whether a given path matches some includes (if not null) and no excludes.
180: * @param path a relative file path as described in class Javadoc
181: * @param useKnownIncludes true to also match in case this path is a parent of some known included root
182: * @return true for a match
183: */
184: public boolean matches(String path, boolean useKnownIncludes) {
185: if (path == null) {
186: throw new NullPointerException();
187: }
188: if (excludePattern != null
189: && excludePattern.matcher(path).matches()) {
190: return false;
191: }
192: if (includePattern != null) {
193: if (includePattern.matcher(path).matches()) {
194: return true;
195: }
196: if (useKnownIncludes
197: && (path.length() == 0 || path.endsWith("/"))) {
198: for (String incl : knownIncludes) {
199: if (incl.startsWith(path)) {
200: return true;
201: }
202: }
203: }
204: return false;
205: } else {
206: return true;
207: }
208: }
209:
210: /**
211: * Find folders which match although their parent folders do not; or folders
212: * which do not match but which contain files which do.
213: * <ul>
214: * <li>Wildcard-free folder include paths, such as <samp>foo/bar/</samp> (or the
215: * equivalent <samp>foo/bar/**</samp>), are returned directly if they are not excluded.
216: * <li>Wildcard-using paths trigger a scan of the provided root directory.
217: * Any actual files or folders found beneath that root which {@link #matches match}
218: * are noted, and their minimal paths are returned.
219: * <li>If a file matches but its containing folder does not, and the file exists,
220: * the folder is listed as an include root.
221: * <li>If this matcher has a null includes list, just the root folder is returned.
222: * </ul>
223: * @return a set of minimal included folders
224: */
225: public Set<File> findIncludedRoots()
226: throws IllegalArgumentException {
227: if (includes == null) {
228: return Collections.singleton(base);
229: }
230: Set<File> roots = new HashSet<File>();
231: if (base != null) {
232: for (String incl : knownIncludes) {
233: roots.add(new File(base, incl.replace('/',
234: File.separatorChar)));
235: }
236: }
237: return roots;
238: }
239:
240: private Set<String> computeKnownIncludes() {
241: if (includes == null) {
242: return Collections.emptySet();
243: }
244: SortedSet<String> roots = new TreeSet<String>();
245: StringTokenizer patternstok = new StringTokenizer(includes,
246: ", "); // NOI18N
247: boolean search = false;
248: while (patternstok.hasMoreTokens()) {
249: String pattern = patternstok.nextToken().replace('\\', '/')
250: .replaceFirst("/\\*\\*$", "/"); // NOI18N
251: if (pattern.equals("**")) { // NOI18N
252: roots.add(""); // NOI18N
253: } else if (pattern.indexOf('*') == -1
254: && pattern.endsWith("/")) { // NOI18N
255: // Optimize in case all includes are wildcard-free paths from root.
256: if (excludePattern == null
257: || !excludePattern.matcher(pattern).matches()) {
258: String parent = pattern
259: .substring(0, pattern.lastIndexOf('/',
260: pattern.length() - 2) + 1);
261: if (!includePattern.matcher(parent).matches()) {
262: roots.add(pattern);
263: }
264: }
265: } else if (base != null) {
266: // Optimization failed. Need to search for actual matches.
267: search = true;
268: }
269: }
270: // Verify that roots really exist, even if they are wilcard-free.
271: if (base != null && base.isDirectory()) {
272: Iterator<String> it = roots.iterator();
273: while (it.hasNext()) {
274: if (!new File(base, it.next().replace('/',
275: File.separatorChar)).isDirectory()) {
276: it.remove();
277: }
278: }
279: }
280: if (search) {
281: // Find what dirs inside root actually match the path, so we known which parents to include later.
282: // XXX note that this fails to listen to file creations & deletions inside the root so the result
283: // can become inaccurate. Not clear how to efficiently solve that.
284: findMatches(base, "", roots);
285: }
286: return roots;
287: }
288:
289: private void findMatches(File dir, String prefix, Set<String> roots) {
290: assert prefix.length() == 0 || prefix.endsWith("/");
291: assert includes != null;
292: String[] childnames = dir.list();
293: if (childnames == null) {
294: return;
295: }
296: for (String childname : childnames) {
297: File child = new File(dir, childname);
298: boolean isdir = child.isDirectory();
299: String path = prefix + childname;
300: if (isdir) {
301: path += "/"; // NOI18N
302: }
303: if (excludePattern != null
304: && excludePattern.matcher(path).matches()) {
305: continue; // prune
306: }
307: if (includePattern.matcher(path).matches()) {
308: if (isdir) {
309: roots.add(path);
310: } else {
311: roots.add(prefix);
312: }
313: } else if (isdir) {
314: findMatches(child, path, roots);
315: }
316: }
317: }
318:
319: @Override
320: public String toString() {
321: return "PathMatcher[includes=" + includes + ",excludes="
322: + excludes + "]"; // NOI18N
323: }
324:
325: }
|