Merge branch 'db/javac-ast'
[idea/community.git] / platform / lvcs-impl / src / com / intellij / history / integration / IdeaGateway.java
1 /*
2  * Copyright 2000-2014 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.history.integration;
17
18 import com.intellij.history.core.LocalHistoryFacade;
19 import com.intellij.history.core.Paths;
20 import com.intellij.history.core.StoredContent;
21 import com.intellij.history.core.tree.DirectoryEntry;
22 import com.intellij.history.core.tree.Entry;
23 import com.intellij.history.core.tree.FileEntry;
24 import com.intellij.history.core.tree.RootEntry;
25 import com.intellij.openapi.application.ApplicationManager;
26 import com.intellij.openapi.editor.Document;
27 import com.intellij.openapi.fileEditor.FileDocumentManager;
28 import com.intellij.openapi.fileTypes.FileType;
29 import com.intellij.openapi.fileTypes.FileTypeManager;
30 import com.intellij.openapi.project.Project;
31 import com.intellij.openapi.project.ProjectManager;
32 import com.intellij.openapi.roots.ProjectFileIndex;
33 import com.intellij.openapi.roots.ProjectRootManager;
34 import com.intellij.openapi.util.Clock;
35 import com.intellij.openapi.util.Comparing;
36 import com.intellij.openapi.util.Key;
37 import com.intellij.openapi.util.Pair;
38 import com.intellij.openapi.util.text.StringUtil;
39 import com.intellij.openapi.vfs.*;
40 import com.intellij.openapi.vfs.encoding.EncodingRegistry;
41 import com.intellij.openapi.vfs.newvfs.ManagingFS;
42 import com.intellij.openapi.vfs.newvfs.NewVirtualFile;
43 import com.intellij.openapi.vfs.newvfs.events.VFileEvent;
44 import com.intellij.openapi.vfs.newvfs.impl.VirtualFileSystemEntry;
45 import com.intellij.util.NullableFunction;
46 import com.intellij.util.containers.ContainerUtil;
47 import org.jetbrains.annotations.NotNull;
48 import org.jetbrains.annotations.Nullable;
49
50 import java.io.IOException;
51 import java.io.UnsupportedEncodingException;
52 import java.util.ArrayList;
53 import java.util.Arrays;
54 import java.util.Collections;
55 import java.util.List;
56
57 public class IdeaGateway {
58   private static final Key<ContentAndTimestamps> SAVED_DOCUMENT_CONTENT_AND_STAMP_KEY
59     = Key.create("LocalHistory.SAVED_DOCUMENT_CONTENT_AND_STAMP_KEY");
60
61   public boolean isVersioned(@NotNull VirtualFile f) {
62     return isVersioned(f, false);
63   }
64
65   public boolean isVersioned(@NotNull VirtualFile f, boolean shouldBeInContent) {
66     if (!f.isInLocalFileSystem()) return false;
67
68     if (!f.isDirectory() && StringUtil.endsWith(f.getNameSequence(), ".class")) return false;
69
70     LocalHistoryImpl.getInstanceImpl().dispatchPendingEvents();
71
72     VersionedFilterData versionedFilterData;
73     VfsEventDispatchContext vfsEventDispatchContext = ourCurrentEventDispatchContext.get();
74     if (vfsEventDispatchContext != null) {
75       versionedFilterData = vfsEventDispatchContext.myFilterData;
76       if (versionedFilterData == null) versionedFilterData = vfsEventDispatchContext.myFilterData = new VersionedFilterData();
77     } else {
78       versionedFilterData = new VersionedFilterData();
79     }
80
81     boolean isInContent = false;
82     int numberOfOpenProjects = versionedFilterData.myOpenedProjects.size();
83     for (int i = 0; i < numberOfOpenProjects; ++i) {
84       if (Comparing.equal(versionedFilterData.myWorkspaceFiles.get(i), f)) return false;
85       ProjectFileIndex index = versionedFilterData.myProjectFileIndices.get(i);
86
87       if (index.isExcluded(f)) return false;
88       isInContent |= index.isInContent(f);
89     }
90     if (shouldBeInContent && !isInContent) return false;
91
92     // optimisation: FileTypeManager.isFileIgnored(f) already checked inside ProjectFileIndex.isIgnored()
93     return numberOfOpenProjects != 0 || !FileTypeManager.getInstance().isFileIgnored(f);
94   }
95
96   private static final ThreadLocal<VfsEventDispatchContext> ourCurrentEventDispatchContext = new ThreadLocal<>();
97
98   private static class VfsEventDispatchContext {
99     final List<? extends VFileEvent> myEvents;
100     final boolean myBeforeEvents;
101     final VfsEventDispatchContext myPreviousContext;
102
103     VersionedFilterData myFilterData;
104
105     VfsEventDispatchContext(List<? extends VFileEvent> events, boolean beforeEvents, VfsEventDispatchContext context) {
106       myEvents = events;
107       myBeforeEvents = beforeEvents;
108       myPreviousContext = context;
109     }
110
111     public void close() {
112       ourCurrentEventDispatchContext.set(myPreviousContext);
113     }
114   }
115
116   public void runWithVfsEventsDispatchContext(List<? extends VFileEvent> events, boolean beforeEvents, Runnable action) {
117     VfsEventDispatchContext vfsEventDispatchContext = new VfsEventDispatchContext(events, beforeEvents, ourCurrentEventDispatchContext.get());
118     ourCurrentEventDispatchContext.set(vfsEventDispatchContext);
119     try {
120       action.run();
121     } finally {
122       vfsEventDispatchContext.close();
123     }
124   }
125
126   private static class VersionedFilterData {
127     final List<Project> myOpenedProjects = new ArrayList<>();
128     final List<ProjectFileIndex> myProjectFileIndices = new ArrayList<>();
129     final List<VirtualFile> myWorkspaceFiles = new ArrayList<>();
130
131     VersionedFilterData() {
132       Project[] openProjects = ProjectManager.getInstance().getOpenProjects();
133
134       for (Project each : openProjects) {
135         if (each.isDefault()) continue;
136         if (!each.isInitialized()) continue;
137
138         myWorkspaceFiles.add(each.getWorkspaceFile());
139         myOpenedProjects.add(each);
140         myProjectFileIndices.add(ProjectRootManager.getInstance(each).getFileIndex());
141       }
142     }
143   }
144
145   public boolean areContentChangesVersioned(@NotNull VirtualFile f) {
146     return isVersioned(f) && !f.isDirectory() && !f.getFileType().isBinary();
147   }
148
149   public boolean areContentChangesVersioned(@NotNull String fileName) {
150     return !FileTypeManager.getInstance().getFileTypeByFileName(fileName).isBinary();
151   }
152
153   public boolean ensureFilesAreWritable(@NotNull Project p, @NotNull List<VirtualFile> ff) {
154     ReadonlyStatusHandler h = ReadonlyStatusHandler.getInstance(p);
155     return !h.ensureFilesWritable(VfsUtilCore.toVirtualFileArray(ff)).hasReadonlyFiles();
156   }
157
158   @Nullable
159   public VirtualFile findVirtualFile(@NotNull String path) {
160     return LocalFileSystem.getInstance().findFileByPath(path);
161   }
162
163   @NotNull
164   public VirtualFile findOrCreateFileSafely(@NotNull VirtualFile parent, @NotNull String name, boolean isDirectory) throws IOException {
165     VirtualFile f = parent.findChild(name);
166     if (f != null && f.isDirectory() != isDirectory) {
167       f.delete(this);
168       f = null;
169     }
170     if (f == null) {
171       f = isDirectory
172           ? parent.createChildDirectory(this, name)
173           : parent.createChildData(this, name);
174     }
175     return f;
176   }
177
178   @NotNull
179   public VirtualFile findOrCreateFileSafely(@NotNull String path, boolean isDirectory) throws IOException {
180     VirtualFile f = findVirtualFile(path);
181     if (f != null && f.isDirectory() != isDirectory) {
182       f.delete(this);
183       f = null;
184     }
185     if (f == null) {
186       VirtualFile parent = findOrCreateFileSafely(Paths.getParentOf(path), true);
187       String name = Paths.getNameOf(path);
188       f = isDirectory
189           ? parent.createChildDirectory(this, name)
190           : parent.createChildData(this, name);
191     }
192     return f;
193   }
194
195   public List<VirtualFile> getAllFilesFrom(@NotNull String path) {
196     VirtualFile f = findVirtualFile(path);
197     if (f == null) return Collections.emptyList();
198     return collectFiles(f, new ArrayList<>());
199   }
200
201   @NotNull
202   private static List<VirtualFile> collectFiles(@NotNull VirtualFile f, @NotNull List<VirtualFile> result) {
203     if (f.isDirectory()) {
204       for (VirtualFile child : iterateDBChildren(f)) {
205         collectFiles(child, result);
206       }
207     }
208     else {
209       result.add(f);
210     }
211     return result;
212   }
213
214   @NotNull
215   public static Iterable<VirtualFile> iterateDBChildren(VirtualFile f) {
216     if (!(f instanceof NewVirtualFile)) return Collections.emptyList();
217     NewVirtualFile nf = (NewVirtualFile)f;
218     return nf.iterInDbChildren();
219   }
220
221   @NotNull
222   public static Iterable<VirtualFile> loadAndIterateChildren(VirtualFile f) {
223     if (!(f instanceof NewVirtualFile)) return Collections.emptyList();
224     NewVirtualFile nf = (NewVirtualFile)f;
225     return Arrays.asList(nf.getChildren());
226   }
227
228   @NotNull
229   public RootEntry createTransientRootEntry() {
230     ApplicationManager.getApplication().assertReadAccessAllowed();
231     RootEntry root = new RootEntry();
232     doCreateChildren(root, getLocalRoots(), false);
233     return root;
234   }
235
236   @NotNull
237   public RootEntry createTransientRootEntryForPathOnly(@NotNull String path) {
238     ApplicationManager.getApplication().assertReadAccessAllowed();
239     RootEntry root = new RootEntry();
240     doCreateChildrenForPathOnly(root, path, getLocalRoots());
241     return root;
242   }
243
244   private static List<VirtualFile> getLocalRoots() {
245     return Arrays.asList(ManagingFS.getInstance().getLocalRoots());
246   }
247
248   private void doCreateChildrenForPathOnly(@NotNull DirectoryEntry parent,
249                                            @NotNull String path,
250                                            @NotNull Iterable<VirtualFile> children) {
251     for (VirtualFile child : children) {
252       String name = StringUtil.trimStart(child.getName(), "/"); // on Mac FS root name is "/"
253       if (!path.startsWith(name)) continue;
254       String rest = path.substring(name.length());
255       if (!rest.isEmpty() && rest.charAt(0) != '/') continue;
256       if (!rest.isEmpty() && rest.charAt(0) == '/') {
257         rest = rest.substring(1);
258       }
259       Entry e = doCreateEntryForPathOnly(child, rest);
260       if (e == null) continue;
261       parent.addChild(e);
262     }
263   }
264
265   @Nullable
266   private Entry doCreateEntryForPathOnly(@NotNull VirtualFile file, @NotNull String path) {
267     if (!file.isDirectory()) {
268       if (!isVersioned(file)) return null;
269
270       return doCreateFileEntry(file, getActualContentNoAcquire(file));
271     }
272     DirectoryEntry newDir = new DirectoryEntry(file.getName());
273     doCreateChildrenForPathOnly(newDir, path, iterateDBChildren(file));
274     if (!isVersioned(file) && newDir.getChildren().isEmpty()) return null;
275     return newDir;
276   }
277
278   @Nullable
279   public Entry createTransientEntry(@NotNull VirtualFile file) {
280     ApplicationManager.getApplication().assertReadAccessAllowed();
281     return doCreateEntry(file, false);
282   }
283
284   @Nullable
285   public Entry createEntryForDeletion(@NotNull VirtualFile file) {
286     ApplicationManager.getApplication().assertReadAccessAllowed();
287     return doCreateEntry(file, true);
288   }
289
290   @Nullable
291   private Entry doCreateEntry(@NotNull VirtualFile file, boolean forDeletion) {
292     if (!file.isDirectory()) {
293       if (!isVersioned(file)) return null;
294
295       Pair<StoredContent, Long> contentAndStamps;
296       if (forDeletion) {
297         FileDocumentManager m = FileDocumentManager.getInstance();
298         Document d = m.isFileModified(file) ? m.getCachedDocument(file) : null; // should not try to load document
299         contentAndStamps = acquireAndClearCurrentContent(file, d);
300       }
301       else {
302         contentAndStamps = getActualContentNoAcquire(file);
303       }
304
305       return doCreateFileEntry(file, contentAndStamps);
306     }
307
308     DirectoryEntry newDir = null;
309     if (file instanceof VirtualFileSystemEntry) {
310       int nameId = ((VirtualFileSystemEntry)file).getNameId();
311       if (nameId > 0) {
312         newDir = new DirectoryEntry(nameId);
313       }
314     }
315
316     if (newDir == null) {
317       newDir = new DirectoryEntry(file.getName());
318     }
319
320     doCreateChildren(newDir, iterateDBChildren(file), forDeletion);
321     if (!isVersioned(file) && newDir.getChildren().isEmpty()) return null;
322     return newDir;
323   }
324
325   @NotNull
326   private Entry doCreateFileEntry(@NotNull VirtualFile file, Pair<StoredContent, Long> contentAndStamps) {
327     if (file instanceof VirtualFileSystemEntry) {
328       return new FileEntry(((VirtualFileSystemEntry)file).getNameId(), contentAndStamps.first, contentAndStamps.second, !file.isWritable());
329     }
330     return new FileEntry(file.getName(), contentAndStamps.first, contentAndStamps.second, !file.isWritable());
331   }
332
333   private void doCreateChildren(@NotNull DirectoryEntry parent, Iterable<VirtualFile> children, final boolean forDeletion) {
334     List<Entry> entries = ContainerUtil.mapNotNull(children, (NullableFunction<VirtualFile, Entry>)each -> doCreateEntry(each, forDeletion));
335     parent.addChildren(entries);
336   }
337
338   public void registerUnsavedDocuments(@NotNull final LocalHistoryFacade vcs) {
339     ApplicationManager.getApplication().runReadAction(() -> {
340       vcs.beginChangeSet();
341       for (Document d : FileDocumentManager.getInstance().getUnsavedDocuments()) {
342         VirtualFile f = getFile(d);
343         if (!shouldRegisterDocument(f)) continue;
344         registerDocumentContents(vcs, f, d);
345       }
346       vcs.endChangeSet(null);
347     });
348   }
349
350   private boolean shouldRegisterDocument(@Nullable VirtualFile f) {
351     return f != null && f.isValid() && areContentChangesVersioned(f);
352   }
353
354   private void registerDocumentContents(@NotNull LocalHistoryFacade vcs, @NotNull VirtualFile f, Document d) {
355     Pair<StoredContent, Long> contentAndStamp = acquireAndUpdateActualContent(f, d);
356     if (contentAndStamp != null) {
357       vcs.contentChanged(f.getPath(), contentAndStamp.first, contentAndStamp.second);
358     }
359   }
360
361   // returns null is content has not been changes since last time
362   @Nullable
363   public Pair<StoredContent, Long> acquireAndUpdateActualContent(@NotNull VirtualFile f, @Nullable Document d) {
364     ContentAndTimestamps contentAndStamp = f.getUserData(SAVED_DOCUMENT_CONTENT_AND_STAMP_KEY);
365     if (contentAndStamp == null) {
366       if (d != null) saveDocumentContent(f, d);
367       return Pair.create(StoredContent.acquireContent(f), f.getTimeStamp());
368     }
369
370     // if no need to save current document content when simply return and clear stored one
371     if (d == null) {
372       f.putUserData(SAVED_DOCUMENT_CONTENT_AND_STAMP_KEY, null);
373       return Pair.create(contentAndStamp.content, contentAndStamp.registeredTimestamp);
374     }
375
376     // if the stored content equals the current one, do not store it and return null
377     if (d.getModificationStamp() == contentAndStamp.documentModificationStamp) return null;
378
379     // is current content has been changed, store it and return the previous one
380     saveDocumentContent(f, d);
381     return Pair.create(contentAndStamp.content, contentAndStamp.registeredTimestamp);
382   }
383
384   private static void saveDocumentContent(@NotNull VirtualFile f, @NotNull Document d) {
385     f.putUserData(SAVED_DOCUMENT_CONTENT_AND_STAMP_KEY,
386                   new ContentAndTimestamps(Clock.getTime(),
387                                            StoredContent.acquireContent(bytesFromDocument(d)),
388                                            d.getModificationStamp()));
389   }
390
391   @NotNull
392   public Pair<StoredContent, Long> acquireAndClearCurrentContent(@NotNull VirtualFile f, @Nullable Document d) {
393     ContentAndTimestamps contentAndStamp = f.getUserData(SAVED_DOCUMENT_CONTENT_AND_STAMP_KEY);
394     f.putUserData(SAVED_DOCUMENT_CONTENT_AND_STAMP_KEY, null);
395
396     if (d != null && contentAndStamp != null) {
397       // if previously stored content was not changed, return it
398       if (d.getModificationStamp() == contentAndStamp.documentModificationStamp) {
399         return Pair.create(contentAndStamp.content, contentAndStamp.registeredTimestamp);
400       }
401     }
402
403     // release previously stored
404     if (contentAndStamp != null) {
405       contentAndStamp.content.release();
406     }
407
408     // take document's content if any
409     if (d != null) {
410       return Pair.create(StoredContent.acquireContent(bytesFromDocument(d)), Clock.getTime());
411     }
412
413     return Pair.create(StoredContent.acquireContent(f), f.getTimeStamp());
414   }
415
416   @NotNull
417   private static Pair<StoredContent, Long> getActualContentNoAcquire(@NotNull VirtualFile f) {
418     ContentAndTimestamps result = f.getUserData(SAVED_DOCUMENT_CONTENT_AND_STAMP_KEY);
419     if (result == null) {
420       return Pair.create(StoredContent.transientContent(f), f.getTimeStamp());
421     }
422     return Pair.create(result.content, result.registeredTimestamp);
423   }
424
425   private static byte[] bytesFromDocument(@NotNull Document d) {
426     try {
427       return d.getText().getBytes(getFile(d).getCharset().name());
428     }
429     catch (UnsupportedEncodingException e) {
430       return d.getText().getBytes();
431     }
432   }
433
434   public String stringFromBytes(@NotNull byte[] bytes, @NotNull String path) {
435     try {
436       VirtualFile file = findVirtualFile(path);
437       if (file == null) {
438         return CharsetToolkit.bytesToString(bytes, EncodingRegistry.getInstance().getDefaultCharset());
439       }
440       return new String(bytes, file.getCharset().name());
441     }
442     catch (UnsupportedEncodingException e1) {
443       return new String(bytes);
444     }
445   }
446
447   public void saveAllUnsavedDocuments() {
448     FileDocumentManager.getInstance().saveAllDocuments();
449   }
450
451   @Nullable
452   private static VirtualFile getFile(@NotNull Document d) {
453     return FileDocumentManager.getInstance().getFile(d);
454   }
455
456   @Nullable
457   public Document getDocument(@NotNull String path) {
458     return FileDocumentManager.getInstance().getDocument(findVirtualFile(path));
459   }
460
461   @NotNull
462   public FileType getFileType(@NotNull String fileName) {
463     return FileTypeManager.getInstance().getFileTypeByFileName(fileName);
464   }
465
466   private static class ContentAndTimestamps {
467     long registeredTimestamp;
468     StoredContent content;
469     long documentModificationStamp;
470
471     private ContentAndTimestamps(long registeredTimestamp, StoredContent content, long documentModificationStamp) {
472       this.registeredTimestamp = registeredTimestamp;
473       this.content = content;
474       this.documentModificationStamp = documentModificationStamp;
475     }
476   }
477 }