Cleanup: NotNull/Nullable
[idea/community.git] / java / compiler / impl / src / com / intellij / compiler / impl / TranslatingCompilerFilesMonitor.java
1 /*
2  * Copyright 2000-2017 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.compiler.impl;
17
18 import com.intellij.compiler.server.BuildManager;
19 import com.intellij.openapi.application.ApplicationManager;
20 import com.intellij.openapi.application.PathManager;
21 import com.intellij.openapi.diagnostic.Logger;
22 import com.intellij.openapi.fileTypes.FileTypeManager;
23 import com.intellij.openapi.project.Project;
24 import com.intellij.openapi.project.ProjectManager;
25 import com.intellij.openapi.project.ProjectUtil;
26 import com.intellij.openapi.roots.ProjectRootManager;
27 import com.intellij.openapi.util.Comparing;
28 import com.intellij.openapi.util.io.FileUtil;
29 import com.intellij.openapi.vfs.*;
30 import com.intellij.openapi.vfs.newvfs.BulkFileListener;
31 import com.intellij.openapi.vfs.newvfs.NewVirtualFile;
32 import com.intellij.openapi.vfs.newvfs.events.*;
33 import com.intellij.util.messages.MessageBus;
34 import gnu.trove.THashSet;
35 import org.jetbrains.annotations.NotNull;
36 import org.jetbrains.annotations.Nullable;
37
38 import java.io.File;
39 import java.util.Collection;
40 import java.util.List;
41 import java.util.Set;
42
43 /**
44  * @author Eugene Zhuravlev
45  *
46  * A source file is scheduled for recompilation if
47  * 1. its timestamp has changed
48  * 2. one of its corresponding output files was deleted
49  * 3. output root of containing module has changed
50  *
51  * An output file is scheduled for deletion if:
52  * 1. corresponding source file has been scheduled for recompilation (see above)
53  * 2. corresponding source file has been deleted
54  */
55 public class TranslatingCompilerFilesMonitor implements BulkFileListener {
56   private static final Logger LOG = Logger.getInstance("#com.intellij.compiler.impl.TranslatingCompilerFilesMonitor");
57   private final BuildManager myBuildManager;
58
59   public TranslatingCompilerFilesMonitor(MessageBus bus, BuildManager buildManager) {
60     myBuildManager = buildManager;
61     bus.connect().subscribe(VirtualFileManager.VFS_CHANGES, this);
62   }
63
64   public static TranslatingCompilerFilesMonitor getInstance() {
65     return ApplicationManager.getApplication().getComponent(TranslatingCompilerFilesMonitor.class);
66   }
67
68   @FunctionalInterface
69   private interface FileProcessor {
70     void execute(@NotNull VirtualFile file);
71   }
72
73   private static void processRecursively(@NotNull VirtualFile fromFile, final boolean dbOnly, @NotNull FileProcessor processor) {
74     if (!(fromFile.getFileSystem() instanceof LocalFileSystem)) {
75       return;
76     }
77
78     VfsUtilCore.visitChildrenRecursively(fromFile, new VirtualFileVisitor() {
79       @NotNull @Override
80       public Result visitFileEx(@NotNull VirtualFile file) {
81         if (isIgnoredByBuild(file)) {
82           return SKIP_CHILDREN;
83         }
84
85         if (!file.isDirectory()) {
86           processor.execute(file);
87         }
88         return CONTINUE;
89       }
90
91       @Nullable
92       @Override
93       public Iterable<VirtualFile> getChildrenIterable(@NotNull VirtualFile file) {
94         if (dbOnly) {
95           return file.isDirectory()? ((NewVirtualFile)file).iterInDbChildren() : null;
96         }
97         if (file.equals(fromFile) || !file.isDirectory()) {
98           return null; // skipping additional checks for the initial file and non-directory files
99         }
100         // optimization: for all files that are not under content of currently opened projects iterate over DB children
101         return isInContentOfOpenedProject(file)? null : ((NewVirtualFile)file).iterInDbChildren();
102       }
103     });
104   }
105
106   private static boolean isInContentOfOpenedProject(@NotNull final VirtualFile file) {
107     // probably need a read action to ensure that the project was not disposed during the iteration over the project list
108     for (Project project : ProjectManager.getInstance().getOpenProjects()) {
109       if (!project.isInitialized() || !BuildManager.getInstance().isProjectWatched(project)) {
110         continue;
111       }
112       if (ProjectRootManager.getInstance(project).getFileIndex().isInContent(file)) {
113         return true;
114       }
115     }
116     return false;
117   }
118
119   @Override
120   public void before(@NotNull List<? extends VFileEvent> events) {
121     Collection<File> filesDeleted = new THashSet<>(FileUtil.FILE_HASHING_STRATEGY);
122     for (VFileEvent event : events) {
123       if (event instanceof VFileDeleteEvent || event instanceof VFileMoveEvent) {
124         final VirtualFile file = event.getFile();
125         if (file != null) {
126           collectPaths(file, filesDeleted);
127         }
128       }
129     }
130     notifyFilesDeleted(filesDeleted);
131   }
132
133   @Override
134   public void after(@NotNull List<? extends VFileEvent> events) {
135     final Set<File> filesDeleted = new THashSet<>(FileUtil.FILE_HASHING_STRATEGY);
136     final Set<File> filesChanged = new THashSet<>(FileUtil.FILE_HASHING_STRATEGY);
137     for (VFileEvent event : events) {
138       if (event instanceof VFilePropertyChangeEvent) {
139         handlePropChange((VFilePropertyChangeEvent)event, filesDeleted, filesChanged);
140       }
141       else if (event instanceof VFileMoveEvent || event instanceof VFileCreateEvent || event instanceof VFileContentChangeEvent || event instanceof VFileCopyEvent) {
142         VirtualFile file = event.getFile();
143         if (file != null) {
144           collectPaths(file, filesChanged);
145         }
146       }
147     }
148
149     // If a file name differs ony in case, on case-insensitive file systems such name still denotes the same file.
150     // In this situation filesDeleted and filesChanged sets will contain paths wchich are different only in case.
151     // Thus the order in which BuildManager is notified, is important:
152     // first deleted paths notification and only then changed paths notification
153     notifyFilesDeleted(filesDeleted);
154     notifyFilesChanged(filesChanged);
155   }
156
157   private static void handlePropChange(@NotNull VFilePropertyChangeEvent event,
158                                        @NotNull Collection<? super File> filesDeleted,
159                                        @NotNull Collection<? super File> filesChanged) {
160     if (VirtualFile.PROP_NAME.equals(event.getPropertyName())) {
161       final String oldName = (String)event.getOldValue();
162       final String newName = (String)event.getNewValue();
163       if (Comparing.equal(oldName, newName)) {
164         // Old and new names may actually be the same: sometimes such events are sent by VFS
165         return;
166       }
167       final VirtualFile eventFile = event.getFile();
168       if (isInContentOfOpenedProject(eventFile)) {
169         final VirtualFile parent = eventFile.getParent();
170         if (parent != null) {
171           final String root = parent.getPath() + "/" + oldName;
172           if (eventFile.isDirectory()) {
173             VfsUtilCore.visitChildrenRecursively(eventFile, new VirtualFileVisitor() {
174               private final StringBuilder filePath = new StringBuilder(root);
175
176               @Override
177               public boolean visitFile(@NotNull VirtualFile child) {
178                 if (child.isDirectory()) {
179                   if (!Comparing.equal(child, eventFile)) {
180                     filePath.append("/").append(child.getName());
181                   }
182                 }
183                 else {
184                   String childPath = filePath.toString();
185                   if (!Comparing.equal(child, eventFile)) {
186                     childPath += "/" + child.getName();
187                   }
188                   filesDeleted.add(new File(childPath));
189                 }
190                 return true;
191               }
192
193               @Override
194               public void afterChildrenVisited(@NotNull VirtualFile file) {
195                 if (file.isDirectory() && !Comparing.equal(file, eventFile)) {
196                   filePath.delete(filePath.length() - file.getName().length() - 1, filePath.length());
197                 }
198               }
199             });
200           }
201           else {
202             filesDeleted.add(new File(root));
203           }
204         }
205         collectPaths(eventFile, filesChanged);
206       }
207     }
208   }
209
210   private static void collectPaths(@NotNull VirtualFile file, @NotNull Collection<? super File> outFiles) {
211     if (!isIgnoredOrUnderIgnoredDirectory(file)) {
212       processRecursively(file, !isInContentOfOpenedProject(file), f -> outFiles.add(new File(f.getPath())));
213     }
214   }
215
216   private static boolean isIgnoredOrUnderIgnoredDirectory(@NotNull VirtualFile file) {
217     if (isIgnoredByBuild(file)) {
218       return true;
219     }
220     final FileTypeManager fileTypeManager = FileTypeManager.getInstance();
221     VirtualFile current = file.getParent();
222     while (current != null) {
223       if (fileTypeManager.isFileIgnored(current)) {
224         return true;
225       }
226       current = current.getParent();
227     }
228     return false;
229   }
230
231   private static boolean isIgnoredByBuild(@NotNull VirtualFile file) {
232     return
233         FileTypeManager.getInstance().isFileIgnored(file) ||
234         ProjectUtil.isProjectOrWorkspaceFile(file)        ||
235         FileUtil.isAncestor(PathManager.getConfigPath(), file.getPath(), false); // is config file
236   }
237
238   private void notifyFilesChanged(@NotNull Collection<? extends File> paths) {
239     if (!paths.isEmpty()) {
240       myBuildManager.notifyFilesChanged(paths);
241     }
242   }
243
244   private void notifyFilesDeleted(@NotNull Collection<? extends File> paths) {
245     if (!paths.isEmpty()) {
246       myBuildManager.notifyFilesDeleted(paths);
247     }
248   }
249 }