/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* 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.intellij.openapi.fileEditor.impl;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ex.ProjectEx;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.NotNullLazyKey;
import com.intellij.openapi.util.UserDataHolder;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.vfs.*;
import com.intellij.openapi.vfs.ex.temp.TempFileSystem;
import com.intellij.util.ArrayUtil;
import com.intellij.util.NullableFunction;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import java.io.File;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class NonProjectFileWritingAccessProvider extends WritingAccessProvider {
private static final Key<Boolean> ENABLE_IN_TESTS = Key.create("NON_PROJECT_FILE_ACCESS_ENABLE_IN_TESTS");
private static final NotNullLazyKey<AtomicInteger, UserDataHolder> ACCESS_ALLOWED =
NotNullLazyKey.create("NON_PROJECT_FILE_ACCESS", holder -> new AtomicInteger());
private static final AtomicBoolean myInitialized = new AtomicBoolean();
@NotNull private final Project myProject;
@Nullable private static NullableFunction<List<VirtualFile>, UnlockOption> ourCustomUnlocker;
@TestOnly
public static void setCustomUnlocker(@Nullable NullableFunction<List<VirtualFile>, UnlockOption> unlocker) {
ourCustomUnlocker = unlocker;
}
public NonProjectFileWritingAccessProvider(@NotNull Project project) {
myProject = project;
if (myInitialized.compareAndSet(false, true)) {
VirtualFileManager.getInstance().addVirtualFileListener(new OurVirtualFileAdapter());
}
}
@Override
public boolean isPotentiallyWritable(@NotNull VirtualFile file) {
return true;
}
@NotNull
@Override
public Collection<VirtualFile> requestWriting(VirtualFile... files) {
if (isAllAccessAllowed()) return Collections.emptyList();
List<VirtualFile> deniedFiles = Stream.of(files).filter(o -> !isWriteAccessAllowed(o, myProject)).collect(Collectors.toList());
if (deniedFiles.isEmpty()) return Collections.emptyList();
UnlockOption unlockOption = askToUnlock(deniedFiles);
if (unlockOption == null) return deniedFiles;
switch (unlockOption) {
case UNLOCK:
allowWriting(deniedFiles);
break;
case UNLOCK_DIR:
allowWriting(ContainerUtil.map(deniedFiles, VirtualFile::getParent));
break;
case UNLOCK_ALL:
ACCESS_ALLOWED.getValue(getApp()).incrementAndGet();
break;
}
return Collections.emptyList();
}
@Nullable
private UnlockOption askToUnlock(@NotNull List<VirtualFile> files) {
if (ourCustomUnlocker != null) return ourCustomUnlocker.fun(files);
NonProjectFileWritingAccessDialog dialog = new NonProjectFileWritingAccessDialog(myProject, files);
if (!dialog.showAndGet()) return null;
return dialog.getUnlockOption();
}
public static boolean isWriteAccessAllowed(@NotNull VirtualFile file, @NotNull Project project) {
if (isAllAccessAllowed()) return true;
if (file.isDirectory()) return true;
if (!(file.getFileSystem() instanceof LocalFileSystem)) return true; // do not block e.g., HttpFileSystem, LightFileSystem etc.
if (file.getFileSystem() instanceof TempFileSystem) return true;
if (ArrayUtil.contains(file, IdeDocumentHistory.getInstance(project).getChangedFiles())) return true;
if (!getApp().isUnitTestMode() && FileUtil.isAncestor(new File(FileUtil.getTempDirectory()), VfsUtilCore.virtualToIoFile(file), true)) {
return true;
}
VirtualFile each = file;
while (each != null) {
if (ACCESS_ALLOWED.getValue(each).get() > 0) return true;
each = each.getParent();
}
return isProjectFile(file, project);
}
private static boolean isProjectFile(@NotNull VirtualFile file, @NotNull Project project) {
for (NonProjectFileWritingAccessExtension each : Extensions.getExtensions(NonProjectFileWritingAccessExtension.EP_NAME, project)) {
if (each.isWritable(file)) return true;
if (each.isNotWritable(file)) return false;
}
ProjectFileIndex fileIndex = ProjectFileIndex.SERVICE.getInstance(project);
if (fileIndex.isInContent(file)) return true;
if (!Registry.is("ide.hide.excluded.files") && fileIndex.isExcluded(file) && !fileIndex.isUnderIgnored(file)) return true;
return false;
}
public static void allowWriting(VirtualFile... allowedFiles) {
allowWriting(Arrays.asList(allowedFiles));
}
public static void allowWriting(Iterable<VirtualFile> allowedFiles) {
for (VirtualFile eachAllowed : allowedFiles) {
ACCESS_ALLOWED.getValue(eachAllowed).incrementAndGet();
}
}
public static void disableChecksDuring(@NotNull Runnable runnable) {
Application app = getApp();
ACCESS_ALLOWED.getValue(app).incrementAndGet();
try {
runnable.run();
}
finally {
ACCESS_ALLOWED.getValue(app).decrementAndGet();
}
}
@TestOnly
public static void enableChecksInTests(@NotNull Disposable disposable) {
getApp().putUserData(ENABLE_IN_TESTS, Boolean.TRUE);
getApp().putUserData(ACCESS_ALLOWED, null);
Disposer.register(disposable, () -> {
getApp().putUserData(ENABLE_IN_TESTS, null);
getApp().putUserData(ACCESS_ALLOWED, null);
});
}
private static boolean isAllAccessAllowed() {
Application app = getApp();
// disable checks in tests, if not asked
if (app.isUnitTestMode() && app.getUserData(ENABLE_IN_TESTS) != Boolean.TRUE) {
return true;
}
return ACCESS_ALLOWED.getValue(app).get() > 0;
}
private static Application getApp() {
return ApplicationManager.getApplication();
}
public enum UnlockOption {UNLOCK, UNLOCK_DIR, UNLOCK_ALL}
private static class OurVirtualFileAdapter extends VirtualFileAdapter {
@Override
public void fileCreated(@NotNull VirtualFileEvent event) {
unlock(event);
}
@Override
public void fileCopied(@NotNull VirtualFileCopyEvent event) {
unlock(event);
}
private static void unlock(@NotNull VirtualFileEvent event) {
if (!event.isFromRefresh() && !event.getFile().isDirectory()) allowWriting(event.getFile());
}
}
}