package org.infernus.idea.checkstyle.checker;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.RuntimeInterruptedException;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileVisitor;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.infernus.idea.checkstyle.CheckStylePlugin;
import org.infernus.idea.checkstyle.exception.CheckStylePluginException;
import org.infernus.idea.checkstyle.model.ConfigurationLocation;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import static com.intellij.openapi.util.Pair.pair;
import static java.util.Collections.emptyMap;
import static org.infernus.idea.checkstyle.checker.ConfigurationLocationResult.resultOf;
import static org.infernus.idea.checkstyle.checker.ConfigurationLocationStatus.BLACKLISTED;
import static org.infernus.idea.checkstyle.checker.ConfigurationLocationStatus.NOT_PRESENT;
import static org.infernus.idea.checkstyle.checker.ConfigurationLocationStatus.PRESENT;
public class ScanFiles implements Callable<Map<PsiFile, List<Problem>>> {
private static final Log LOG = LogFactory.getLog(ScanFiles.class);
private final List<PsiFile> files;
private final Map<Module, Set<PsiFile>> moduleToFiles;
private final Set<ScannerListener> listeners = new HashSet<>();
private final CheckStylePlugin plugin;
private final ConfigurationLocation overrideConfigLocation;
public ScanFiles(@NotNull final CheckStylePlugin checkStylePlugin,
@NotNull final List<VirtualFile> virtualFiles,
@Nullable final ConfigurationLocation overrideConfigLocation) {
this.plugin = checkStylePlugin;
this.overrideConfigLocation = overrideConfigLocation;
files = findAllFilesFor(virtualFiles);
moduleToFiles = mapsModulesToFiles();
}
private List<PsiFile> findAllFilesFor(@NotNull final List<VirtualFile> virtualFiles) {
final List<PsiFile> childFiles = new ArrayList<>();
final PsiManager psiManager = PsiManager.getInstance(this.plugin.getProject());
for (final VirtualFile virtualFile : virtualFiles) {
childFiles.addAll(buildFilesList(psiManager, virtualFile));
}
return childFiles;
}
private Map<Module, Set<PsiFile>> mapsModulesToFiles() {
final Map<Module, Set<PsiFile>> modulesToFiles = new HashMap<>();
for (final PsiFile file : files) {
final Module module = ModuleUtil.findModuleForPsiElement(file);
Set<PsiFile> filesForModule = modulesToFiles.get(module);
if (filesForModule == null) {
filesForModule = new HashSet<>();
modulesToFiles.put(module, filesForModule);
}
filesForModule.add(file);
}
return modulesToFiles;
}
@Override
public final Map<PsiFile, List<Problem>> call() {
try {
fireCheckStarting(files);
final Pair<ConfigurationLocationResult, Map<PsiFile, List<Problem>>> scanResult =
processFilesForModuleInfoAndScan();
return checkComplete(scanResult.first, scanResult.second);
} catch (final RuntimeInterruptedException e) {
LOG.debug("Scan cancelled by IDEA", e);
return checkComplete(resultOf(PRESENT), emptyMap());
} catch (final CheckStylePluginException e) {
LOG.error("An error occurred while scanning a file.", e);
fireErrorCaught(e);
return checkComplete(resultOf(PRESENT), emptyMap());
} catch (final Throwable e) {
LOG.error("An error occurred while scanning a file.", e);
fireErrorCaught(new CheckStylePluginException("An error occurred while scanning a file.", e));
return checkComplete(resultOf(PRESENT), emptyMap());
}
}
private Map<PsiFile, List<Problem>> checkComplete(final ConfigurationLocationResult configurationLocationResult,
final Map<PsiFile, List<Problem>> filesToProblems) {
fireCheckComplete(configurationLocationResult, filesToProblems);
return filesToProblems;
}
public void addListener(final ScannerListener listener) {
listeners.add(listener);
}
private void fireCheckStarting(final List<PsiFile> filesToScan) {
listeners.forEach(listener -> listener.scanStarting(filesToScan));
}
private void fireCheckComplete(final ConfigurationLocationResult configLocationResult,
final Map<PsiFile, List<Problem>> fileResults) {
listeners.forEach(listener -> listener.scanComplete(configLocationResult, fileResults));
}
private void fireErrorCaught(final CheckStylePluginException error) {
listeners.forEach(listener -> listener.errorCaught(error));
}
private void fireFilesScanned(final int count) {
listeners.forEach(listener -> listener.filesScanned(count));
}
private List<PsiFile> buildFilesList(final PsiManager psiManager, final VirtualFile virtualFile) {
final List<PsiFile> allChildFiles = new ArrayList<>();
ApplicationManager.getApplication().runReadAction(() -> {
final FindChildFiles visitor = new FindChildFiles(virtualFile, psiManager);
VfsUtilCore.visitChildrenRecursively(virtualFile, visitor);
allChildFiles.addAll(visitor.locatedFiles);
});
return allChildFiles;
}
private Pair<ConfigurationLocationResult, Map<PsiFile, List<Problem>>> processFilesForModuleInfoAndScan() {
final Map<PsiFile, List<Problem>> fileResults = new HashMap<>();
for (final Module module : moduleToFiles.keySet()) {
if (module == null) {
continue;
}
final ConfigurationLocationResult locationResult = configurationLocation(overrideConfigLocation, module);
if (locationResult.status != PRESENT) {
return pair(locationResult, emptyMap());
}
final Set<PsiFile> filesForModule = moduleToFiles.get(module);
if (filesForModule.isEmpty()) {
continue;
}
fileResults.putAll(filesWithProblems(filesForModule, checkFiles(module, filesForModule, locationResult
.location)));
fireFilesScanned(filesForModule.size());
}
return pair(resultOf(PRESENT), fileResults);
}
@NotNull
private Map<PsiFile, List<Problem>> filesWithProblems(final Set<PsiFile> filesForModule, final Map<PsiFile,
List<Problem>> moduleFileResults) {
final Map<PsiFile, List<Problem>> moduleResults = new HashMap<>();
for (final PsiFile psiFile : filesForModule) {
final List<Problem> resultsForFile = moduleFileResults.get(psiFile);
if (resultsForFile != null && !resultsForFile.isEmpty()) {
moduleResults.put(psiFile, new ArrayList<>(resultsForFile));
}
}
return moduleResults;
}
@NotNull
private ConfigurationLocationResult configurationLocation(final ConfigurationLocation override, final Module
module) {
final ConfigurationLocation location = plugin.getConfigurationLocation(module, override);
if (location == null) {
return resultOf(NOT_PRESENT);
}
if (location.isBlacklisted()) {
return resultOf(location, BLACKLISTED);
}
return resultOf(location, PRESENT);
}
private Map<PsiFile, List<Problem>> checkFiles(final Module module, final Set<PsiFile> filesToScan, final
ConfigurationLocation configurationLocation) {
final List<ScannableFile> scannableFiles = new ArrayList<>();
try {
scannableFiles.addAll(ScannableFile.createAndValidate(filesToScan, plugin, module));
return checkerFactory(module.getProject()).checker(module, configurationLocation)
.map(checker -> checker.scan(scannableFiles, plugin.getConfiguration().isSuppressingErrors()))
.orElseGet(Collections::emptyMap);
} finally {
scannableFiles.forEach(ScannableFile::deleteIfRequired);
}
}
private CheckerFactory checkerFactory(final Project project) {
return ServiceManager.getService(project, CheckerFactory.class);
}
private class FindChildFiles extends VirtualFileVisitor {
private final VirtualFile virtualFile;
private final PsiManager psiManager;
public final List<PsiFile> locatedFiles = new ArrayList<>();
FindChildFiles(final VirtualFile virtualFile, final PsiManager psiManager) {
this.virtualFile = virtualFile;
this.psiManager = psiManager;
}
@Override
public boolean visitFile(@NotNull final VirtualFile file) {
if (!file.isDirectory()) {
final PsiFile psiFile = psiManager.findFile(virtualFile);
if (psiFile != null) {
locatedFiles.add(psiFile);
}
}
return true;
}
}
}