/*
* 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;
import com.intellij.openapi.components.AbstractProjectComponent;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ContentIterator;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.*;
import com.intellij.psi.search.FilenameIndex;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.util.containers.ContainerUtil;
import gnu.trove.THashSet;
import mobi.hsz.idea.gitignore.util.Constants;
import mobi.hsz.idea.gitignore.util.MatcherUtil;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Pattern;
/**
* Cache that retrieves matching files using given {@link Pattern}.
* It uses {@link VirtualFileListener} to handle changes in the files tree and clear cached entries
* for the specific pattern parts.
*
* @author Jakub Chrzanowski <jakub@hsz.mobi>
* @since 1.3.1
*/
public class FilesIndexCacheProjectComponent extends AbstractProjectComponent {
/** Concurrent cache map. */
@NotNull
private final ConcurrentMap<String, Collection<VirtualFile>> cacheMap;
/** {@link VirtualFileManager} instance. */
@NotNull
private final VirtualFileManager virtualFileManager;
/** {@link VirtualFileListener} instance to watch for operations on the filesystem. */
@NotNull
private final VirtualFileListener virtualFileListener = new VirtualFileAdapter() {
@Override
public void propertyChanged(@NotNull VirtualFilePropertyEvent event) {
if (event.getPropertyName().equals("name")) {
removeAffectedCaches(event);
}
}
@Override
public void fileCreated(@NotNull VirtualFileEvent event) {
removeAffectedCaches(event);
}
@Override
public void fileDeleted(@NotNull VirtualFileEvent event) {
removeAffectedCaches(event);
}
@Override
public void fileMoved(@NotNull VirtualFileMoveEvent event) {
removeAffectedCaches(event);
}
@Override
public void fileCopied(@NotNull VirtualFileCopyEvent event) {
removeAffectedCaches(event);
}
@Override
public void beforeFileMovement(@NotNull VirtualFileMoveEvent event) {
removeAffectedCaches(event);
}
private void removeAffectedCaches(@NotNull VirtualFileEvent event) {
for (String key : cacheMap.keySet()) {
List<String> parts = StringUtil.split(key, Constants.DOLLAR);
if (MatcherUtil.matchAnyPart(parts.toArray(new String[parts.size()]), event.getFile().getPath())) {
cacheMap.remove(key);
}
}
}
};
/**
* Returns {@link FilesIndexCacheProjectComponent} service instance.
*
* @param project current project
* @return {@link FilesIndexCacheProjectComponent instance}
*/
public static FilesIndexCacheProjectComponent getInstance(@NotNull final Project project) {
return project.getComponent(FilesIndexCacheProjectComponent.class);
}
/**
* Initializes {@link #cacheMap} and {@link VirtualFileManager}.
*
* @param project current project
*/
protected FilesIndexCacheProjectComponent(@NotNull final Project project) {
super(project);
cacheMap = ContainerUtil.newConcurrentMap();
virtualFileManager = VirtualFileManager.getInstance();
}
/** Registers {@link #virtualFileListener} when project is opened. */
@Override
public void projectOpened() {
virtualFileManager.addVirtualFileListener(virtualFileListener);
}
/** Unregisters {@link #virtualFileListener} when project is closed. */
@Override
public void projectClosed() {
virtualFileManager.removeVirtualFileListener(virtualFileListener);
cacheMap.clear();
}
/**
* Finds {@link VirtualFile} instances for the specific {@link Pattern} and caches them.
*
* @param project current project
* @param pattern to handle
* @return matched files list
*/
@NotNull
public Collection<VirtualFile> getFilesForPattern(@NotNull final Project project, @NotNull Pattern pattern) {
final GlobalSearchScope scope = GlobalSearchScope.allScope(project);
final String[] parts = MatcherUtil.getParts(pattern);
if (parts.length > 0) {
final String key = StringUtil.join(parts, Constants.DOLLAR);
if (cacheMap.get(key) == null) {
final THashSet<VirtualFile> files = new THashSet<VirtualFile>(1000);
ProjectRootManager.getInstance(project).getFileIndex().iterateContent(new ContentIterator() {
@Override
public boolean processFile(VirtualFile fileOrDir) {
final String name = fileOrDir.getName();
if (MatcherUtil.matchAnyPart(parts, name)) {
for (VirtualFile file : FilenameIndex.getVirtualFilesByName(project, name, scope)) {
if (file.isValid() && MatcherUtil.matchAllParts(parts, file.getPath())) {
files.add(file);
}
}
}
return true;
}
});
cacheMap.put(key, files);
}
return cacheMap.get(key);
}
return ContainerUtil.newArrayList();
}
/**
* Returns component's name.
*
* @return component's name
*/
@NotNull
@Override
public String getComponentName() {
return "FilesIndexCacheProjectComponent";
}
}