/*
* The MIT License (MIT)
*
* Copyright (c) 2017 hsz Jakub Chrzanowski <jakub@hsz.mobi>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package mobi.hsz.idea.gitignore.util;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileVisitor;
import com.intellij.util.containers.ContainerUtil;
import mobi.hsz.idea.gitignore.IgnoreBundle;
import mobi.hsz.idea.gitignore.psi.IgnoreEntry;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
/**
* Glob util class that prepares glob statements or searches for content using glob rules.
*
* @author Jakub Chrzanowski <jakub@hsz.mobi>
* @since 0.5
*/
public class Glob {
/** Cache map that holds processed regex statements to the glob rules. */
private static final HashMap<String, String> CACHE = ContainerUtil.newHashMap();
/** Private constructor to prevent creating {@link Glob} instance. */
private Glob() {
}
/**
* Finds for {@link VirtualFile} list using glob rule in given root directory.
*
* @param root root directory
* @param entry ignore entry
* @return search result
*/
@NotNull
public static List<VirtualFile> find(@NotNull final VirtualFile root, @NotNull IgnoreEntry entry) {
return find(root, entry, false);
}
/**
* Finds for {@link VirtualFile} list using glob rule in given root directory.
*
* @param root root directory
* @param entry ignore entry
* @param includeNested attach children to the search result
* @return search result
*/
@NotNull
public static List<VirtualFile> find(@NotNull final VirtualFile root, @NotNull IgnoreEntry entry,
final boolean includeNested) {
final Pattern pattern = createPattern(entry);
if (pattern == null) {
return Collections.emptyList();
}
final List<VirtualFile> files = ContainerUtil.newArrayList();
final Matcher matcher = pattern.matcher("");
VirtualFileVisitor<Matcher> visitor = new VirtualFileVisitor<Matcher>(VirtualFileVisitor.NO_FOLLOW_SYMLINKS) {
@Override
public boolean visitFile(@NotNull VirtualFile file) {
boolean matches = false;
String path = Utils.getRelativePath(root, file);
if (path == null || Utils.isVcsDirectory(file)) {
return false;
}
if (getCurrentValue() == null || MatcherUtil.match(getCurrentValue(), path)) {
matches = true;
files.add(file);
}
setValueForChildren(includeNested && matches ? null : getCurrentValue());
return true;
}
};
visitor.setValueForChildren(matcher);
VfsUtil.visitChildrenRecursively(root, visitor);
return files;
}
/**
* Finds for {@link VirtualFile} paths list using glob rule in given root directory.
*
* @param root root directory
* @param entry ignore entry
* @return search result
*/
@NotNull
public static List<String> findAsPaths(@NotNull VirtualFile root, @NotNull IgnoreEntry entry) {
return findAsPaths(root, entry, false);
}
/**
* Finds for {@link VirtualFile} paths list using glob rule in given root directory.
*
* @param root root directory
* @param entry ignore entry
* @param includeNested attach children to the search result
* @return search result
*/
@NotNull
public static List<String> findAsPaths(@NotNull VirtualFile root,
@NotNull IgnoreEntry entry,
boolean includeNested) {
final List<String> list = ContainerUtil.newArrayList();
final List<VirtualFile> files = find(root, entry, includeNested);
for (VirtualFile file : files) {
list.add(Utils.getRelativePath(root, file));
}
return list;
}
/**
* Creates regex {@link Pattern} using glob rule.
*
* @param rule rule value
* @param syntax rule syntax
* @return regex {@link Pattern}
*/
@Nullable
public static Pattern createPattern(@NotNull String rule, @NotNull IgnoreBundle.Syntax syntax) {
return createPattern(rule, syntax, false);
}
/**
* Creates regex {@link Pattern} using glob rule.
*
* @param rule rule value
* @param syntax rule syntax
* @param acceptChildren Matches directory children
* @return regex {@link Pattern}
*/
@Nullable
public static Pattern createPattern(@NotNull String rule,
@NotNull IgnoreBundle.Syntax syntax,
boolean acceptChildren) {
final String regex = syntax.equals(IgnoreBundle.Syntax.GLOB) ? createRegex(rule, acceptChildren) : rule;
try {
return Pattern.compile(regex);
} catch (PatternSyntaxException e) {
return null;
}
}
/**
* Creates regex {@link Pattern} using {@link IgnoreEntry}.
*
* @param entry {@link IgnoreEntry}
* @return regex {@link Pattern}
*/
@Nullable
public static Pattern createPattern(@NotNull IgnoreEntry entry) {
return createPattern(entry, false);
}
/**
* Creates regex {@link Pattern} using {@link IgnoreEntry}.
*
* @param entry {@link IgnoreEntry}
* @param acceptChildren Matches directory children
* @return regex {@link Pattern}
*/
@Nullable
public static Pattern createPattern(@NotNull IgnoreEntry entry, boolean acceptChildren) {
return createPattern(entry.getValue(), entry.getSyntax(), acceptChildren);
}
/**
* Creates regex {@link String} using glob rule.
*
* @param glob rule
* @param acceptChildren Matches directory children
* @return regex {@link String}
*/
@NotNull
public static String createRegex(@NotNull String glob, boolean acceptChildren) {
glob = glob.trim();
String cached = CACHE.get(glob);
if (cached != null) {
return cached;
}
StringBuilder sb = new StringBuilder("^");
boolean escape = false, star = false, doubleStar = false, bracket = false;
int beginIndex = 0;
if (StringUtil.startsWith(glob, Constants.DOUBLESTAR)) {
sb.append("(?:[^/]*?/)*");
beginIndex = 2;
doubleStar = true;
} else if (StringUtil.startsWith(glob, "*/")) {
sb.append("[^/]*");
beginIndex = 1;
star = true;
} else if (StringUtil.equals(Constants.STAR, glob)) {
sb.append(".*");
} else if (StringUtil.startsWithChar(glob, '*')) {
sb.append(".*?");
} else if (StringUtil.startsWithChar(glob, '/')) {
beginIndex = 1;
} else {
int slashes = StringUtil.countChars(glob, '/');
if (slashes == 0 || (slashes == 1 && StringUtil.endsWithChar(glob, '/'))) {
sb.append("(?:[^/]*?/)*");
}
}
char[] chars = glob.substring(beginIndex).toCharArray();
for (char ch : chars) {
if (bracket && ch != ']') {
sb.append(ch);
continue;
} else if (doubleStar) {
doubleStar = false;
if (ch == '/') {
sb.append("(?:[^/]*/)*?");
continue;
} else {
sb.append("[^/]*?");
}
}
if (ch == '*') {
if (escape) {
sb.append("\\*");
escape = false;
star = false;
} else if (star) {
char prev = sb.length() > 0 ? sb.charAt(sb.length() - 1) : '\0';
if (prev == '\0' || prev == '^' || prev == '/') {
doubleStar = true;
} else {
sb.append("[^/]*?");
}
star = false;
} else {
star = true;
}
continue;
} else if (star) {
sb.append("[^/]*?");
star = false;
}
switch (ch) {
case '\\':
if (escape) {
sb.append("\\\\");
escape = false;
} else {
escape = true;
}
break;
case '?':
if (escape) {
sb.append("\\?");
escape = false;
} else {
sb.append('.');
}
break;
case '[':
if (escape) {
sb.append('\\');
escape = false;
} else {
bracket = true;
}
sb.append(ch);
break;
case ']':
if (!bracket) {
sb.append('\\');
}
sb.append(ch);
bracket = false;
escape = false;
break;
case '.':
case '(':
case ')':
case '+':
case '|':
case '^':
case '$':
case '@':
case '%':
sb.append('\\');
sb.append(ch);
escape = false;
break;
default:
escape = false;
sb.append(ch);
}
}
if (star || doubleStar) {
if (StringUtil.endsWithChar(sb, '/')) {
sb.append(acceptChildren ? ".+" : "[^/]+/?");
} else {
sb.append("[^/]*/?");
}
} else {
if (StringUtil.endsWithChar(sb, '/')) {
if (acceptChildren) {
sb.append("[^/]*");
}
} else {
sb.append(acceptChildren ? "(?:/.*)?" : "/?");
}
}
sb.append('$');
CACHE.put(glob, sb.toString());
return sb.toString();
}
/** Clears {@link Glob#CACHE} cache. */
public static void clearCache() {
CACHE.clear();
}
}