/* * 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 org.apache.tools.ant.types.ZipScanner; import java.util.regex.Pattern; /** * A singleton class that provides blazingly fast implementation of the default * excludes of Ant's {@link org.apache.tools.ant.DirectoryScanner}, assuming * case-sensitiveness. * * TODO: this class needs to be revisited, when Gwt's Ant is upgraded. * * Currently, we do not go to ant if (a) the filterList is empty, or (b) the * filterList has "common" patterns. Exception: When path ends in '/', we defer * to ant. * * TODO: This code could be made more general and cleaner by removing the * dependency on Ant completely. All ant patterns could be compiled into * reg-exps. That could also make the code faster. Plus, at several places, * Ant's documentation seems to be incomplete. Instead, perhaps, we should * specify our own rules for writing patterns. */ public class DefaultFilters { /** * Constants to represent the type of files that will be filtered. */ public static enum FilterFileType { RESOURCE_FILES(null), // JAVA_FILES(".java"), // CLASS_FILES(".class"); private final String suffix; /* used when defaultExcludes is false */ private final ResourceFilter justThisFileTypeFilter = new ResourceFilter() { public boolean allows(String path) { return defaultAntIncludes.allows(path) && matches(path); } }; private final ResourceFilter defaultFilter = new ResourceFilter() { public boolean allows(String path) { return getFileTypeFilter().allows(path) && !isDefaultExcluded(path); } }; private FilterFileType(String suffix) { this.suffix = suffix; } public ResourceFilter getDefaultFilter() { return defaultFilter; } /* used when defaultExcludes is false */ public ResourceFilter getFileTypeFilter() { return justThisFileTypeFilter; } public String getSuffix() { return suffix; } public boolean matches(String path) { if (suffix == null) { return true; } return path.endsWith(suffix); } } // \w (word character), ., $, /, -, *, ~, #, % private static final Pattern antPattern = Pattern.compile("^[\\w\\.\\$/\\-\\*~#%]*$"); // accepts all but paths starting with '/'. Default include list is '**' private static final ResourceFilter defaultAntIncludes = new ResourceFilter() { public boolean allows(String path) { return path.charAt(0) != '/'; } }; /** * @return <code>true</code> if given path should be excluded from resources. */ private static boolean isDefaultExcluded(String path) { // CVS if (path.endsWith("/CVS") || path.contains("/CVS/") || path.startsWith("CVS/") || path.endsWith("/.cvsignore")) { return true; } // Subversion if (path.endsWith("/.svn") || path.contains("/.svn/") || path.startsWith(".svn/") || path.endsWith("/.svnignore")) { return true; } // Git if (path.endsWith("/.git") || path.contains("/.git/") || path.startsWith(".git/") || path.endsWith("/.gitignore")) { return true; } // SCCS if (path.endsWith("/SCCS") || path.contains("/SCCS/")) { return true; } // Visual SourceSafe if (path.endsWith("/vssver.scc")) { return true; } // Mac if (path.endsWith("/.DS_Store")) { return true; } return false; } /** * Returns a pattern string that can be passed in Java Pattern.compile(..). * For spec, see <a href="http://www.jajakarta.org/ant/ant-1.6.1/docs/ja/manual/api/org/apache/tools/ant/DirectoryScanner.html" * >DirectoryScanner</a> From the spec: There is a special case regarding the * use of File.separators at the beginning of the pattern and the string to * match: When a pattern starts with a File.separator, the string to match * must also start with a File.separator. When a pattern does not start with a * File.separator, the string to match may not start with a File.separator. * * </p> * * TODO: This method could accept all ant patterns, but then all characters * that have a special meaning in Java's regular expression would need to be * escaped. * * @param antPatternString the ant pattern String. * @return a pattern string that can be passed in Java's Pattern.compile(..), * null if cannot process the pattern. */ static String getPatternFromAntPattern(String antPatternString) { if (!antPattern.matcher(antPatternString).matches()) { return null; } // do not handle patterns that have *** if (antPatternString.indexOf("***") != -1) { return null; } if (antPatternString.endsWith("/")) { /* * From the DirectoryScanner.html spec: When a pattern ends with a '/' or * '\', "**" is appended. if ant pattern = testing/, path = testing/foo, * result = true. */ antPatternString = antPatternString + "**"; } StringBuffer sb = new StringBuffer(); int length = antPatternString.length(); for (int i = 0; i < length; i++) { char c = antPatternString.charAt(i); switch (c) { case '.': sb.append("\\."); break; case '$': sb.append("\\$"); break; case '/': // convert /** to (/[^/]*)* except when / is the first char. if (i != 0 && i + 2 < length && antPatternString.charAt(i + 1) == '*' && antPatternString.charAt(i + 2) == '*') { sb.append("(/[^/]*)*"); i += 2; // handled 2 more chars than usual } else { sb.append(c); } break; case '*': // ** to .* if (i + 1 < length && antPatternString.charAt(i + 1) == '*') { if (i + 2 < length && antPatternString.charAt(i + 2) == '/') { if (i == 0) { /* * When a pattern does not start with a File.separator, the * string to match may not start with a File.separator. */ sb.append("([^/]+/)*"); } else { // convert **/ to ([^/]*/)* sb.append("([^/]*/)*"); } i += 2; } else { if (i == 0) { /* * When a pattern does not start with a File.separator, the * string to match may not start with a File.separator. */ sb.append("([^/].*)*"); } else { sb.append(".*"); } i++; } } else { sb.append("[^/]*"); } break; default: sb.append(c); break; } } return sb.toString(); } static ZipScanner getScanner(String[] includeList, String[] excludeList, String[] skipList, boolean defaultExcludes, boolean caseSensitive) { /* * Hijack Ant's ZipScanner to handle inclusions/exclusions exactly as Ant * does. We're only using its pattern-matching capabilities; the code path * I'm using never tries to hit the filesystem in Ant 1.6.5. */ ZipScanner scanner = new ZipScanner(); if (includeList.length > 0) { scanner.setIncludes(includeList); } if (excludeList.length > 0 || skipList.length > 0) { String[] excludeOrSkip = concatenate(excludeList, skipList); scanner.setExcludes(excludeOrSkip); } if (defaultExcludes) { scanner.addDefaultExcludes(); } scanner.setCaseSensitive(caseSensitive); scanner.init(); return scanner; } private static String[] concatenate(String[] array1, String[] array2) { String[] answer = new String[array1.length + array2.length]; int i = 0; for (String entry : array1) { answer[i++] = entry; } for (String entry : array2) { answer[i++] = entry; } return answer; } private static Pattern getPatternFromStrings(String... patterns) { StringBuffer entirePattern = new StringBuffer("^"); int length = patterns.length; int count = 0; for (String pattern : patterns) { entirePattern.append("(" + pattern + ")"); if (count < length - 1) { entirePattern.append("|"); } count++; } entirePattern.append("$"); return Pattern.compile(entirePattern.toString()); } private final ResourceFilter rejectAll = new ResourceFilter() { public boolean allows(String path) { return false; } }; public ResourceFilter customClassFilesFilter(String includeList[], String excludeList[], String skipList[], boolean defaultExcludes, boolean caseSensitive) { return getCustomFilter(includeList, excludeList, skipList, defaultExcludes, caseSensitive, FilterFileType.CLASS_FILES); } public ResourceFilter customJavaFilter(String includeList[], String excludeList[], String skipList[], boolean defaultExcludes, boolean caseSensitive) { return getCustomFilter(includeList, excludeList, skipList, defaultExcludes, caseSensitive, FilterFileType.JAVA_FILES); } public ResourceFilter customResourceFilter(String includeList[], String excludeList[], String[] skipList, boolean defaultExcludes, boolean caseSensitive) { return getCustomFilter(includeList, excludeList, skipList, defaultExcludes, caseSensitive, FilterFileType.RESOURCE_FILES); } /** * Return a customResourceFiter that handles all the argument. If unable to * create a customResourceFilter that handles the arguments, catchAll is used * as the final ResourceFilter. */ ResourceFilter customFilterWithCatchAll(final String includeList[], final String excludeList[], final String skipList[], final boolean defaultExcludes, final ResourceFilter catchAll, final FilterFileType filterFileType) { assert includeList.length > 0 || excludeList.length > 0 || skipList.length > 0; final ResourceFilter includeFilter = getIncludesFilterPart(includeList); final ResourceFilter excludeFilter = getExcludesFilterPart(concatenate( excludeList, skipList)); if (includeFilter == null || excludeFilter == null) { return catchAll; } // another common-case ResourceFilter filter = new ResourceFilter() { public boolean allows(String path) { // do not handle the case when pattern ends in '/' if (path.endsWith("/")) { return catchAll.allows(path); } return isPathAllowedByDefaults(path, defaultExcludes, filterFileType) && includeFilter.allows(path) && !excludeFilter.allows(path); } private boolean isPathAllowedByDefaults(String path, boolean defaultExcludes, FilterFileType filterFileType) { boolean fileTypeMatch = filterFileType.matches(path); if (!fileTypeMatch) { return false; } if (defaultExcludes) { return !isDefaultExcluded(path); } return true; } }; return filter; } ResourceFilter getCustomFilter(final String includeList[], final String excludeList[], final String skipList[], final boolean defaultExcludes, final boolean caseSensitive, final FilterFileType filterFileType) { if (includeList.length == 0 && excludeList.length == 0 && skipList.length == 0 && caseSensitive) { // optimize for the common case. return getMatchingDefaultFilter(defaultExcludes, filterFileType); } // don't create a catchAll in default cases ResourceFilter catchAll = new ResourceFilter() { ZipScanner scanner = getScanner(includeList, excludeList, skipList, defaultExcludes, caseSensitive); public boolean allows(String path) { return filterFileType.matches(path) && scanner.match(path); } }; // for now, don't handle case sensitivity if (!caseSensitive) { return catchAll; } return customFilterWithCatchAll(includeList, excludeList, skipList, defaultExcludes, catchAll, filterFileType); } ResourceFilter getExcludesFilterPart(final String list[]) { return getFilterPart(list, false); } ResourceFilter getIncludesFilterPart(final String list[]) { return getFilterPart(list, true); } /** * @param list patterns to add to the filter. * @param isInclude Only used if the the array is empty. If <code>true</code> * treat this as an include. Otherwise, assume this is an excludes * filter and exclude all files. * @return */ private ResourceFilter getFilterPart(final String list[], final boolean isInclude) { if (list.length == 0) { return isInclude ? defaultAntIncludes : rejectAll; } String patternStrings[] = new String[list.length]; int count = 0; for (String antPatternString : list) { String patternString = getPatternFromAntPattern(antPatternString); if (patternString == null) { return null; } patternStrings[count++] = patternString; } final Pattern pattern = getPatternFromStrings(patternStrings); return new ResourceFilter() { public boolean allows(String path) { return pattern.matcher(path).matches(); } }; } /** * Obtain the appropriate resourceFilter based on defaultExcludes and isJava * values. Assumptions: caseSensitive = true,and the includesList and * excludesList are empty */ private ResourceFilter getMatchingDefaultFilter(boolean defaultExcludes, FilterFileType filterFileType) { if (defaultExcludes) { return filterFileType.getDefaultFilter(); } return filterFileType.getFileTypeFilter(); } }