package org.infernus.idea.checkstyle.checker;
import com.intellij.openapi.application.AccessToken;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiJavaFile;
import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
import org.infernus.idea.checkstyle.CheckStylePlugin;
import org.infernus.idea.checkstyle.util.OS;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.stream.Collectors;
import static java.util.Optional.ofNullable;
import static org.infernus.idea.checkstyle.checker.PsiFileValidator.isScannable;
/**
* A representation of a file able to be scanned.
*/
public class ScannableFile {
private static final Logger LOG = LoggerFactory.getLogger(ScannableFile.class);
private static final String TEMPFILE_DIR_PREFIX = "csi-";
private final File realFile;
private final File baseTempDir;
private final PsiFile psiFile;
/**
* Create a new scannable file from a PSI file.
* <p>
* If required this will create a temporary copy of the file.
*
* @param psiFile the psiFile to create the file from.
* @param module the module the file belongs to.
* @throws IOException if file creation is required and fails.
*/
public ScannableFile(@NotNull final PsiFile psiFile,
@Nullable final Module module)
throws IOException {
this.psiFile = psiFile;
if (!existsOnFilesystem(psiFile) || documentIsModifiedAndUnsaved(psiFile)) {
baseTempDir = prepareBaseTmpDirFor(psiFile);
realFile = createTemporaryFileFor(psiFile, module, baseTempDir);
} else {
baseTempDir = null;
realFile = new File(pathOf(psiFile));
}
}
public static List<ScannableFile> createAndValidate(@NotNull final Collection<PsiFile> psiFiles,
@NotNull final CheckStylePlugin plugin,
@Nullable final Module module) {
final AccessToken readAccessToken = ApplicationManager.getApplication().acquireReadActionLock();
try {
return psiFiles.stream()
.filter(psiFile -> isScannable(psiFile, ofNullable(module), plugin.getConfiguration()))
.map(psiFile -> ScannableFile.create(psiFile, module))
.filter(Objects::nonNull)
.collect(Collectors.toList());
} finally {
readAccessToken.finish();
}
}
@Nullable
private static ScannableFile create(@NotNull final PsiFile psiFile,
@Nullable final Module module) {
try {
final CreateScannableFileAction fileAction = new CreateScannableFileAction(psiFile, module);
ApplicationManager.getApplication().runReadAction(fileAction);
//noinspection ThrowableResultOfMethodCallIgnored
if (fileAction.getFailure() != null) {
throw fileAction.getFailure();
}
return fileAction.getFile();
} catch (IOException e) {
LOG.error("Failure when creating temporary file", e);
return null;
}
}
private String pathOf(@NotNull final PsiFile file) {
return virtualFileOf(file)
.map(VirtualFile::getPath)
.orElseThrow(() -> new IllegalStateException("PSIFile does not have associated virtual file: " + file));
}
private File createTemporaryFileFor(@NotNull final PsiFile file,
@Nullable final Module module,
@NotNull final File tempDir)
throws IOException {
final File temporaryFile = new File(parentDirFor(file, module, tempDir), file.getName());
temporaryFile.deleteOnExit();
writeContentsToFile(file, temporaryFile);
return temporaryFile;
}
private File parentDirFor(@NotNull final PsiFile file,
@Nullable final Module module,
@NotNull final File baseTmpDir) {
File tmpDirForFile = null;
if (file.getParent() != null && module != null) {
final String parentUrl = file.getParent().getVirtualFile().getUrl();
for (String moduleSourceRoot : ModuleRootManager.getInstance(module).getContentRootUrls()) {
if (parentUrl.startsWith(moduleSourceRoot)) {
tmpDirForFile = new File(baseTmpDir.getAbsolutePath() + parentUrl.substring(moduleSourceRoot.length()));
break;
}
}
}
if (tmpDirForFile == null) {
if (file instanceof PsiJavaFile) {
final String packageName = ((PsiJavaFile) file).getPackageName();
final String packagePath = packageName.replaceAll(
"\\.", Matcher.quoteReplacement(File.separator));
tmpDirForFile = new File(baseTmpDir.getAbsolutePath() + File.separator + packagePath);
} else {
tmpDirForFile = baseTmpDir;
}
}
//noinspection ResultOfMethodCallIgnored
tmpDirForFile.mkdirs();
return tmpDirForFile;
}
private File prepareBaseTmpDirFor(PsiFile psiFile) {
final File baseTmpDir = new File(temporaryDirectory(psiFile),
TEMPFILE_DIR_PREFIX + UUID.randomUUID().toString());
baseTmpDir.deleteOnExit();
return baseTmpDir;
}
private String temporaryDirectory(PsiFile psiFile) {
String systemTempDir = System.getProperty("java.io.tmpdir");
if (OS.isWindows() && driveLetterOf(systemTempDir) != driveLetterOf(pathOf(psiFile))) {
// Checkstyle requires the files to be on the same drive
final File projectTempDir = new File(psiFile.getProject().getBasePath(), "csi-tmp");
if (projectTempDir.mkdirs()) {
return projectTempDir.getAbsolutePath();
}
}
return systemTempDir;
}
private char driveLetterOf(String windowsPath) {
if (windowsPath != null && windowsPath.length() > 0) {
final Path normalisedPath = Paths.get(windowsPath).normalize().toAbsolutePath();
return normalisedPath.toFile().toString().charAt(0);
}
return '?';
}
private boolean existsOnFilesystem(@NotNull final PsiFile file) {
return virtualFileOf(file)
.map(virtualFile -> LocalFileSystem.getInstance().exists(virtualFile))
.orElse(false);
}
private boolean documentIsModifiedAndUnsaved(final PsiFile file) {
final FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
return virtualFileOf(file)
.filter(fileDocumentManager::isFileModified)
.map(fileDocumentManager::getDocument)
.map(fileDocumentManager::isDocumentUnsaved)
.orElse(false);
}
private void writeContentsToFile(final PsiFile file,
final File outFile)
throws IOException {
final String lineSeparator = CodeStyleSettingsManager.getSettings(file.getProject()).getLineSeparator();
final Writer tempFileOut = writerTo(outFile, charSetOf(file));
for (final char character : file.getText().toCharArray()) {
if (character == '\n') { // IDEA uses \n internally
tempFileOut.write(lineSeparator);
} else {
tempFileOut.write(character);
}
}
tempFileOut.flush();
tempFileOut.close();
}
@NotNull
private Charset charSetOf(final PsiFile file) {
return virtualFileOf(file)
.map(VirtualFile::getCharset)
.orElseGet(() -> Charset.forName("UTF-8"));
}
private Optional<VirtualFile> virtualFileOf(final PsiFile file) {
return ofNullable(file.getVirtualFile());
}
@NotNull
private Writer writerTo(final File outFile, final Charset charset) throws FileNotFoundException {
return new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream(outFile), charset.newEncoder()));
}
public File getFile() {
return realFile;
}
public static void deleteIfRequired(@Nullable final ScannableFile scannableFile) {
if (scannableFile != null) {
scannableFile.deleteIfRequired();
}
}
private void deleteIfRequired() {
if (baseTempDir != null
&& baseTempDir.getName().startsWith(TEMPFILE_DIR_PREFIX)) {
delete(baseTempDir);
}
}
private void delete(@NotNull final File file) {
if (!file.exists()) {
return;
}
if (file.isDirectory()) {
final File[] files = file.listFiles();
if (files != null) {
for (File child : files) {
delete(child);
}
}
}
//noinspection ResultOfMethodCallIgnored
file.delete();
}
public String getAbsolutePath() {
return realFile.getAbsolutePath();
}
public PsiFile getPsiFile() {
return psiFile;
}
@Override
public String toString() {
return String.format("[ScannableFile: file=%s; temporary=%s]", realFile.toString(), baseTempDir != null);
}
}