9a2de59babeb64bdbe7169f5de7e40ae80fd9a60
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / vfs / newvfs / persistent / PersistentFS.java
1 /*
2  * Copyright 2000-2012 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.openapi.vfs.newvfs.persistent;
17
18 import com.google.common.collect.Lists;
19 import com.intellij.openapi.application.ApplicationManager;
20 import com.intellij.openapi.application.ModalityState;
21 import com.intellij.openapi.application.ex.ApplicationEx;
22 import com.intellij.openapi.components.ApplicationComponent;
23 import com.intellij.openapi.diagnostic.Logger;
24 import com.intellij.openapi.util.Pair;
25 import com.intellij.openapi.util.ShutDownTracker;
26 import com.intellij.openapi.util.SystemInfo;
27 import com.intellij.openapi.util.io.BufferExposingByteArrayInputStream;
28 import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream;
29 import com.intellij.openapi.util.io.ByteSequence;
30 import com.intellij.openapi.util.io.FileUtil;
31 import com.intellij.openapi.util.registry.Registry;
32 import com.intellij.openapi.vfs.*;
33 import com.intellij.openapi.vfs.impl.win32.Win32LocalFileSystem;
34 import com.intellij.openapi.vfs.newvfs.*;
35 import com.intellij.openapi.vfs.newvfs.events.*;
36 import com.intellij.openapi.vfs.newvfs.impl.FakeVirtualFile;
37 import com.intellij.openapi.vfs.newvfs.impl.VirtualDirectoryImpl;
38 import com.intellij.openapi.vfs.newvfs.impl.VirtualFileSystemEntry;
39 import com.intellij.util.ArrayUtil;
40 import com.intellij.util.containers.ContainerUtil;
41 import com.intellij.util.containers.StripedLockIntObjectConcurrentHashMap;
42 import com.intellij.util.io.DupOutputStream;
43 import com.intellij.util.io.ReplicatorInputStream;
44 import com.intellij.util.messages.MessageBus;
45 import gnu.trove.THashMap;
46 import gnu.trove.TIntHashSet;
47 import gnu.trove.TIntObjectHashMap;
48 import org.jetbrains.annotations.NonNls;
49 import org.jetbrains.annotations.NotNull;
50 import org.jetbrains.annotations.Nullable;
51
52 import java.io.*;
53 import java.util.*;
54 import java.util.concurrent.atomic.AtomicBoolean;
55
56 /**
57  * @author max
58  */
59 public class PersistentFS extends ManagingFS implements ApplicationComponent {
60   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vfs.newvfs.persistent.PersistentFS");
61
62   private static final int CHILDREN_CACHED_FLAG = 0x01;
63   static final int IS_DIRECTORY_FLAG = 0x02;
64   private static final int IS_READ_ONLY = 0x04;
65   private static final int MUST_RELOAD_CONTENT = 0x08;
66   private static final int IS_SYMLINK = 0x10;
67   private static final int IS_SPECIAL = 0x20;
68
69   static final int ALL_VALID_FLAGS =
70     CHILDREN_CACHED_FLAG | IS_DIRECTORY_FLAG | IS_READ_ONLY | MUST_RELOAD_CONTENT | IS_SYMLINK | IS_SPECIAL;
71
72   private final MessageBus myEventsBus;
73
74   private final Map<String, VirtualFileSystemEntry> myRoots = new THashMap<String, VirtualFileSystemEntry>();
75   private final TIntObjectHashMap<VirtualFileSystemEntry> myRootsById = new TIntObjectHashMap<VirtualFileSystemEntry>();
76   private VirtualFileSystemEntry myFakeRoot;
77   private final Object INPUT_LOCK = new Object();
78
79   public PersistentFS(MessageBus bus) {
80     myEventsBus = bus;
81
82     ShutDownTracker.getInstance().registerShutdownTask(new Runnable() {
83       @Override
84       public void run() {
85         performShutdown();
86       }
87     });
88   }
89
90   @Override
91   public void disposeComponent() {
92     performShutdown();
93   }
94
95   private final AtomicBoolean myShutdownPerformed = new AtomicBoolean(Boolean.FALSE);
96   private void performShutdown() {
97     if (!myShutdownPerformed.getAndSet(Boolean.TRUE)) {
98       LOG.info("VFS dispose started");
99       FSRecords.dispose();
100       LOG.info("VFS dispose completed");
101     }
102   }
103
104   @Override
105   @NonNls
106   @NotNull
107   public String getComponentName() {
108     return "app.component.PersistentFS";
109   }
110
111   @Override
112   public void initComponent() {
113     FSRecords.connect();
114   }
115
116   @Override
117   public boolean areChildrenLoaded(@NotNull final VirtualFile dir) {
118     return areChildrenLoaded(getFileId(dir));
119   }
120
121   public long getCreationTimestamp() {
122     return FSRecords.getCreationTimestamp();
123   }
124
125   private static NewVirtualFileSystem getDelegate(VirtualFile file) {
126     return (NewVirtualFileSystem)file.getFileSystem();
127   }
128
129   @Override
130   public boolean wereChildrenAccessed(@NotNull final VirtualFile dir) {
131     return FSRecords.wereChildrenAccessed(getFileId(dir));
132   }
133
134   @Override
135   @NotNull
136   public String[] list(@NotNull final VirtualFile file) {
137     int id = getFileId(file);
138     int[] childrenIds = FSRecords.list(id);
139     String[] names = listPersisted(childrenIds);
140     if (areChildrenLoaded(id)) {
141       return names;
142     }
143     Pair<String[], int[]> pair = persistAllChildren(file, id, Pair.create(names, childrenIds));
144     return pair.first;
145   }
146
147   @NotNull
148   public static String[] listPersisted(@NotNull VirtualFile file) {
149     return listPersisted(FSRecords.list(getFileId(file)));
150   }
151
152   @NotNull
153   private static String[] listPersisted(@NotNull int[] childrenIds) {
154     String[] names = ArrayUtil.newStringArray(childrenIds.length);
155     for (int i = 0; i < childrenIds.length; i++) {
156       names[i] = FSRecords.getName(childrenIds[i]);
157     }
158     return names;
159   }
160
161   @NotNull
162   private static Pair<String[],int[]> persistAllChildren(@NotNull VirtualFile file, int id, @NotNull Pair<String[],int[]> current) {
163     String[] currentNames = current.first;
164     int[] currentIds = current.second;
165
166     final NewVirtualFileSystem delegate = getDelegate(file);
167     String[] delegateNames = VfsUtil.filterNames(delegate.list(file));
168     if (delegateNames.length == 0 && currentNames.length > 0) {
169       return current;
170     }
171
172     final String[] names;
173     if (currentNames.length == 0) {
174       names = delegateNames;
175     }
176     else {
177       Set<String> allNamesSet = new LinkedHashSet<String>((currentNames.length + delegateNames.length) * 2);
178       ContainerUtil.addAll(allNamesSet, currentNames);
179       ContainerUtil.addAll(allNamesSet, delegateNames);
180       names = ArrayUtil.toStringArray(allNamesSet);
181     }
182
183     final int[] childrenIds = ArrayUtil.newIntArray(names.length);
184
185     for (int i = 0; i < names.length; i++) {
186       final String name = names[i];
187       int idx = ArrayUtil.indexOf(currentNames, name);
188       if (idx >= 0) {
189         childrenIds[i] = currentIds[idx];
190       }
191       else {
192         int childId = createAndCopyRecord(delegate, new FakeVirtualFile(file, name), id);
193         childrenIds[i] = childId;
194       }
195     }
196
197     FSRecords.updateList(id, childrenIds);
198     int flags = FSRecords.getFlags(id);
199     FSRecords.setFlags(id, flags | CHILDREN_CACHED_FLAG, true);
200
201     return Pair.create(names, childrenIds);
202   }
203
204   @NotNull
205   public static int[] listIds(@NotNull VirtualFile parent) {
206     final int parentId = getFileId(parent);
207
208     int[] ids = FSRecords.list(parentId);
209     if (!areChildrenLoaded(parentId)) {
210       String[] names = listPersisted(ids);
211       Pair<String[], int[]> pair = persistAllChildren(parent, parentId, Pair.create(names, ids));
212       return pair.second;
213     }
214
215     return ids;
216   }
217
218   @NotNull
219   public static Pair<String[],int[]> listAll(@NotNull VirtualFile parent) {
220     final int parentId = getFileId(parent);
221
222     Pair<String[], int[]> pair = FSRecords.listAll(parentId);
223     if (!areChildrenLoaded(parentId)) {
224       return persistAllChildren(parent, parentId, pair);
225     }
226
227     return pair;
228   }
229
230
231   private static boolean areChildrenLoaded(final int parentId) {
232     final int mask = CHILDREN_CACHED_FLAG;
233     return (FSRecords.getFlags(parentId) & mask) != 0;
234   }
235
236   @Override
237   @Nullable
238   public DataInputStream readAttribute(@NotNull final VirtualFile file, @NotNull final FileAttribute att) {
239     return FSRecords.readAttribute(getFileId(file), att.getId());
240   }
241
242   @Override
243   @NotNull
244   public DataOutputStream writeAttribute(@NotNull final VirtualFile file, @NotNull final FileAttribute att) {
245     return FSRecords.writeAttribute(getFileId(file), att.getId(), att.isFixedSize());
246   }
247
248   @Nullable
249   private static DataInputStream readContent(VirtualFile file) {
250     return FSRecords.readContent(getFileId(file));
251   }
252
253   @Nullable
254   private static DataInputStream readContentById(int contentId) {
255     return FSRecords.readContentById(contentId);
256   }
257
258   private static DataOutputStream writeContent(VirtualFile file, boolean readOnly) {
259     return FSRecords.writeContent(getFileId(file), readOnly);
260   }
261
262   private static void writeContent(VirtualFile file, ByteSequence content, boolean readOnly) throws IOException {
263     FSRecords.writeContent(getFileId(file), content, readOnly);
264   }
265
266   public int storeUnlinkedContent(byte[] bytes) {
267     return FSRecords.storeUnlinkedContent(bytes);
268   }
269
270   @Override
271   public int getModificationCount(@NotNull final VirtualFile file) {
272     final int id = getFileId(file);
273     return FSRecords.getModCount(id);
274   }
275
276   @Override
277   public int getCheapFileSystemModificationCount() {
278     return FSRecords.getLocalModCount();
279   }
280
281   @Override
282   public int getFilesystemModificationCount() {
283     return FSRecords.getModCount();
284   }
285
286   private static boolean copyRecordFromDelegateFS(final int id, final int parentId, final VirtualFile file, NewVirtualFileSystem delegate) {
287     String name = file.getName();
288
289     if (name.length() > 0 && namesEqual(delegate, name, FSRecords.getName(id))) return false; // TODO: Handle root attributes change.
290
291     if (name.length() == 0) {            // TODO: hack
292       if (areChildrenLoaded(id)) return false;
293     }
294
295     FSRecords.setParent(id, parentId);
296     FSRecords.setName(id, name);
297
298     delegate = replaceWithNativeFS(delegate);
299
300     FSRecords.setTimestamp(id, delegate.getTimeStamp(file));
301
302     boolean directory = delegate.isDirectory(file);
303
304     FSRecords.setLength(id, directory ? -1L : delegate.getLength(file));
305
306     FSRecords.setFlags(id, (directory ? IS_DIRECTORY_FLAG : 0) |
307                            (delegate.isWritable(file) ? 0 : IS_READ_ONLY) |
308                            (delegate.isSymLink(file) ? IS_SYMLINK : 0) |
309                            (delegate.isSpecialFile(file) ? IS_SPECIAL : 0), true);
310
311     return true;
312   }
313
314   @Override
315   public boolean isDirectory(@NotNull final VirtualFile file) {
316     final int id = getFileId(file);
317     return isDirectory(id);
318   }
319
320   public static boolean isDirectory(final int id) {
321     assert id > 0;
322     return (FSRecords.getFlags(id) & IS_DIRECTORY_FLAG) != 0;
323   }
324
325   public static boolean isSymLink(final int id) {
326     assert id > 0;
327     return (FSRecords.getFlags(id) & IS_SYMLINK) != 0;
328   }
329
330   private static int getParent(final int id) {
331     assert id > 0;
332     return FSRecords.getParent(id);
333   }
334
335   private static boolean namesEqual(VirtualFileSystem fs, String n1, String n2) {
336     return ((NewVirtualFileSystem)fs).isCaseSensitive() ? n1.equals(n2) : n1.equalsIgnoreCase(n2);
337   }
338
339   @Override
340   public boolean exists(@NotNull final VirtualFile fileOrDirectory) {
341     return ((VirtualFileWithId)fileOrDirectory).getId() > 0;
342   }
343
344   @Override
345   public long getTimeStamp(@NotNull final VirtualFile file) {
346     final int id = getFileId(file);
347     return FSRecords.getTimestamp(id);
348   }
349
350   @Override
351   public void setTimeStamp(@NotNull final VirtualFile file, final long modstamp) throws IOException {
352     final int id = getFileId(file);
353
354     FSRecords.setTimestamp(id, modstamp);
355     getDelegate(file).setTimeStamp(file, modstamp);
356   }
357
358   private static int getFileId(final VirtualFile file) {
359     final int id = ((VirtualFileWithId)file).getId();
360     if (id <= 0) {
361       throw new InvalidVirtualFileAccessException(file);
362     }
363     return id;
364   }
365
366   @Override
367   public boolean isSymLink(@NotNull VirtualFile file) {
368     return (FSRecords.getFlags(getFileId(file)) & IS_SYMLINK) != 0;
369   }
370
371   @Override
372   public String resolveSymLink(@NotNull VirtualFile file) {
373     throw new UnsupportedOperationException();
374   }
375
376   @Override
377   public boolean isSpecialFile(@NotNull VirtualFile file) {
378     return (FSRecords.getFlags(getFileId(file)) & IS_SPECIAL) != 0;
379   }
380
381   @Override
382   public boolean isWritable(@NotNull final VirtualFile file) {
383     return (FSRecords.getFlags(getFileId(file)) & IS_READ_ONLY) == 0;
384   }
385
386   @Override
387   public void setWritable(@NotNull final VirtualFile file, final boolean writableFlag) throws IOException {
388     getDelegate(file).setWritable(file, writableFlag);
389     processEvent(new VFilePropertyChangeEvent(this, file, VirtualFile.PROP_WRITABLE, isWritable(file), writableFlag, false));
390   }
391
392   public static int getId(final VirtualFile parent, final String childName, NewVirtualFileSystem delegate) {
393     final int parentId = getFileId(parent);
394
395     final int[] children = FSRecords.list(parentId);
396     for (final int childId : children) {
397       if (namesEqual(delegate, childName, FSRecords.getName(childId))) return childId;
398     }
399
400     VirtualFile fake = new FakeVirtualFile(parent, childName);
401     if (delegate.exists(fake)) {
402       int child = createAndCopyRecord(delegate, fake, parentId);
403       FSRecords.updateList(parentId, ArrayUtil.append(children, child));
404       return child;
405     }
406
407     return 0;
408   }
409
410   @Override
411   public long getLength(@NotNull final VirtualFile file) {
412     final int id = getFileId(file);
413
414     long len = FSRecords.getLength(id);
415     if (len == -1) {
416       len = (int)getDelegate(file).getLength(file);
417       FSRecords.setLength(id, len);
418     }
419
420     return len;
421   }
422
423   @Override
424   public VirtualFile copyFile(final Object requestor, @NotNull final VirtualFile file, @NotNull final VirtualFile newParent, @NotNull final String copyName)
425     throws IOException {
426     getDelegate(file).copyFile(requestor, file, newParent, copyName);
427     processEvent(new VFileCopyEvent(requestor, file, newParent, copyName));
428
429     final VirtualFile child = newParent.findChild(copyName);
430     if (child == null) {
431       throw new IOException("Cannot create child");
432     }
433     return child;
434   }
435
436   @Override
437   public VirtualFile createChildDirectory(final Object requestor, @NotNull final VirtualFile parent, @NotNull final String dir) throws IOException {
438     getDelegate(parent).createChildDirectory(requestor, parent, dir);
439     processEvent(new VFileCreateEvent(requestor, parent, dir, true, false));
440
441     final VirtualFile child = parent.findChild(dir);
442     if (child == null) {
443       throw new IOException("Cannot create child directory '" + dir + "' at " + parent.getPath());
444     }
445     return child;
446   }
447
448   @Override
449   public VirtualFile createChildFile(final Object requestor, @NotNull final VirtualFile parent, @NotNull final String file) throws IOException {
450     getDelegate(parent).createChildFile(requestor, parent, file);
451     processEvent(new VFileCreateEvent(requestor, parent, file, false, false));
452
453     final VirtualFile child = parent.findChild(file);
454     if (child == null) {
455       throw new IOException("Cannot create child file '" + file + "' at " + parent.getPath());
456     }
457     return child;
458   }
459
460   @Override
461   public void deleteFile(final Object requestor, @NotNull final VirtualFile file) throws IOException {
462     final NewVirtualFileSystem delegate = getDelegate(file);
463     delegate.deleteFile(requestor, file);
464
465     if (!delegate.exists(file)) {
466       processEvent(new VFileDeleteEvent(requestor, file, false));
467     }
468   }
469
470   @Override
471   public void renameFile(final Object requestor, @NotNull final VirtualFile file, @NotNull final String newName) throws IOException {
472     getDelegate(file).renameFile(requestor, file, newName);
473     processEvent(new VFilePropertyChangeEvent(requestor, file, VirtualFile.PROP_NAME, file.getName(), newName, false));
474   }
475
476   @Override
477   @NotNull
478   public byte[] contentsToByteArray(@NotNull final VirtualFile file) throws IOException {
479     return contentsToByteArray(file, true);
480   }
481
482   @NotNull
483   public byte[] contentsToByteArray(final VirtualFile file, boolean cacheContent) throws IOException {
484     InputStream contentStream = null;
485     boolean reloadFromDelegate;
486     synchronized (INPUT_LOCK) {
487       reloadFromDelegate = mustReloadContent(file) || (contentStream = readContent(file)) == null;
488     }
489
490     if (reloadFromDelegate) {
491       final NewVirtualFileSystem delegate = getDelegate(file);
492       FSRecords.setLength(getFileId(file), delegate.getLength(file));
493       final byte[] content = delegate.contentsToByteArray(file);
494
495       ApplicationEx application = (ApplicationEx)ApplicationManager.getApplication();
496       // we should cache every local files content
497       // because the local history feature is currently depends on this cache,
498       // perforce offline mode as well
499       if ((!delegate.isReadOnly() || !application.isInternal() && !application.isUnitTestMode()) &&
500           content.length <= PersistentFSConstants.FILE_LENGTH_TO_CACHE_THRESHOLD) {
501         synchronized (INPUT_LOCK) {
502           writeContent(file, new ByteSequence(content), delegate.isReadOnly());
503           setFlag(file, MUST_RELOAD_CONTENT, false);
504         }
505       }
506
507       return content;
508     }
509     else {
510       try {
511         final int length = (int)file.getLength();
512         assert length >= 0 : file;
513         return FileUtil.loadBytes(contentStream, length);
514       }
515       catch (IOException e) {
516         throw FSRecords.handleError(e);
517       }
518     }
519   }
520
521   public byte[] contentsToByteArray(int contentId) throws IOException {
522     return FileUtil.loadBytes(readContentById(contentId));
523   }
524
525   @Override
526   @NotNull
527   public InputStream getInputStream(@NotNull final VirtualFile file) throws IOException {
528     synchronized (INPUT_LOCK) {
529       InputStream contentStream;
530       if (mustReloadContent(file) || (contentStream = readContent(file)) == null) {
531         final NewVirtualFileSystem delegate = getDelegate(file);
532         final long len = delegate.getLength(file);
533         FSRecords.setLength(getFileId(file), len);
534         final InputStream nativeStream = delegate.getInputStream(file);
535
536         if (len > PersistentFSConstants.FILE_LENGTH_TO_CACHE_THRESHOLD) return nativeStream;
537         return createReplicator(file, nativeStream, len, delegate.isReadOnly());
538       }
539       else {
540         return contentStream;
541       }
542     }
543   }
544
545   private InputStream createReplicator(final VirtualFile file, final InputStream nativeStream, final long fileLength, final boolean readOnly)
546     throws IOException {
547     if (nativeStream instanceof BufferExposingByteArrayInputStream) {
548       // optimization
549       BufferExposingByteArrayInputStream  byteStream = (BufferExposingByteArrayInputStream )nativeStream;
550       byte[] bytes = byteStream.getInternalBuffer();
551       storeContentToStorage(fileLength, file, readOnly, bytes, bytes.length);
552       return nativeStream;
553     }
554     final BufferExposingByteArrayOutputStream cache = new BufferExposingByteArrayOutputStream((int)fileLength);
555     return new ReplicatorInputStream(nativeStream, cache) {
556       @Override
557       public void close() throws IOException {
558         super.close();
559         storeContentToStorage(fileLength, file, readOnly, cache.getInternalBuffer(), cache.size());
560       }
561     };
562   }
563
564   private void storeContentToStorage(long fileLength,
565                                      VirtualFile file,
566                                      boolean readOnly, byte[] bytes, int bytesLength)
567     throws IOException {
568     synchronized (INPUT_LOCK) {
569       if (bytesLength == fileLength) {
570         writeContent(file, new ByteSequence(bytes, 0, bytesLength), readOnly);
571         setFlag(file, MUST_RELOAD_CONTENT, false);
572       }
573       else {
574         setFlag(file, MUST_RELOAD_CONTENT, true);
575       }
576     }
577   }
578
579   private static boolean mustReloadContent(final VirtualFile file) {
580     return checkFlag(file, MUST_RELOAD_CONTENT) || FSRecords.getLength(getFileId(file)) == -1L;
581   }
582
583   @Override
584   @NotNull
585   public OutputStream getOutputStream(@NotNull final VirtualFile file, final Object requestor, final long modStamp, final long timeStamp)
586     throws IOException {
587     final VFileContentChangeEvent event = new VFileContentChangeEvent(requestor, file, file.getModificationStamp(), modStamp, false);
588
589     final List<VFileContentChangeEvent> events = Collections.singletonList(event);
590
591     final BulkFileListener publisher = myEventsBus.syncPublisher(VirtualFileManager.VFS_CHANGES);
592     publisher.before(events);
593
594     return new ByteArrayOutputStream() {
595       @Override
596       public void close() throws IOException {
597         super.close();
598
599         NewVirtualFileSystem delegate = getDelegate(file);
600         final OutputStream outputStream = delegate.getOutputStream(file, requestor, modStamp, timeStamp);
601
602         //noinspection IOResourceOpenedButNotSafelyClosed
603         final DupOutputStream sink = new DupOutputStream(new BufferedOutputStream(writeContent(file, delegate.isReadOnly())), outputStream) {
604           @Override
605           public void close() throws IOException {
606             try {
607               super.close();
608             }
609             finally {
610               executeTouch(file, false, event.getModificationStamp());
611               publisher.after(events);
612             }
613           }
614         };
615
616         try {
617           sink.write(buf, 0, count);
618         }
619         finally {
620           sink.close();
621         }
622       }
623     };
624   }
625
626   public int acquireContent(VirtualFile file) {
627     return FSRecords.acquireFileContent(getFileId(file));
628   }
629
630   public void releaseContent(int contentId) {
631     FSRecords.releaseContent(contentId);
632   }
633
634   public int getCurrentContentId(VirtualFile file) {
635     return FSRecords.getContentId(getFileId(file));
636   }
637
638   @Override
639   public void moveFile(final Object requestor, @NotNull final VirtualFile file, @NotNull final VirtualFile newParent) throws IOException {
640     getDelegate(file).moveFile(requestor, file, newParent);
641     processEvent(new VFileMoveEvent(requestor, file, newParent));
642   }
643
644   private void processEvent(VFileEvent event) {
645     processEvents(Collections.singletonList(event));
646   }
647
648   private static class EventWrapper {
649     private final VFileDeleteEvent event;
650     private final int id;
651
652     private EventWrapper(final VFileDeleteEvent event, final int id) {
653       this.event = event;
654       this.id = id;
655     }
656   }
657
658   private static final Comparator<EventWrapper> DEPTH_COMPARATOR = new Comparator<EventWrapper>() {
659     @Override
660     public int compare(final EventWrapper o1, final EventWrapper o2) {
661       return o1.event.getFileDepth() - o2.event.getFileDepth();
662     }
663   };
664
665   private static List<? extends VFileEvent> validateEvents(final List<? extends VFileEvent> events) {
666     final List<EventWrapper> deletionEvents = Lists.newArrayList();
667     for (int i = 0, size = events.size(); i < size; i++) {
668       final VFileEvent event = events.get(i);
669       if (event instanceof VFileDeleteEvent && event.isValid()) {
670         deletionEvents.add(new EventWrapper((VFileDeleteEvent)event, i));
671       }
672     }
673
674     ContainerUtil.quickSort(deletionEvents, DEPTH_COMPARATOR);
675
676     final TIntHashSet invalidIDs = new TIntHashSet(deletionEvents.size());
677     final List<VirtualFile> dirsToBeDeleted = new ArrayList<VirtualFile>();
678     nextEvent:
679     for (EventWrapper wrapper : deletionEvents) {
680       final VirtualFile candidate = wrapper.event.getFile();
681       for (VirtualFile file : dirsToBeDeleted) {
682         if (VfsUtilCore.isAncestor(file, candidate, false)) {
683           invalidIDs.add(wrapper.id);
684           continue nextEvent;
685         }
686       }
687
688       if (candidate.isDirectory()) {
689         dirsToBeDeleted.add(candidate);
690       }
691     }
692
693     final List<VFileEvent> filtered = Lists.newArrayListWithCapacity(events.size() - invalidIDs.size());
694     for (int i = 0, size = events.size(); i < size; i++) {
695       final VFileEvent event = events.get(i);
696       if (event.isValid() && !(event instanceof VFileDeleteEvent && invalidIDs.contains(i))) {
697         filtered.add(event);
698       }
699     }
700     return filtered;
701   }
702
703   @Override
704   public void processEvents(@NotNull List<? extends VFileEvent> events) {
705     ApplicationManager.getApplication().assertWriteAccessAllowed();
706
707     events = validateEvents(events);
708
709     BulkFileListener publisher = myEventsBus.syncPublisher(VirtualFileManager.VFS_CHANGES);
710     publisher.before(events);
711     for (VFileEvent event : events) {
712       applyEvent(event);
713     }
714     publisher.after(events);
715   }
716
717   public static final Object LOCK = new Object();
718
719   @Override
720   @Nullable
721   public VirtualFileSystemEntry findRoot(@NotNull final String basePath, @NotNull final NewVirtualFileSystem fs) { // TODO: read/write locks instead of synchronized
722     synchronized (LOCK) {
723       final String rootUrl = fs.getProtocol() + "://" + basePath;
724       VirtualFileSystemEntry root = myRoots.get(rootUrl);
725       if (root == null && basePath.length() == 0) {
726         root = myFakeRoot;
727       }
728       if (root == null) {
729         try {
730           final int rootId = FSRecords.findRootRecord(rootUrl);
731           if (basePath.length() > 0) {
732             root = new VirtualDirectoryImpl(basePath, null, fs, rootId) {
733               @NotNull
734               @Override
735               public String getName() {
736                 final String name = super.getName();
737
738                 // TODO: HACK!!! Get to simpler solution.
739                 if (fs instanceof JarFileSystem) {
740                   String jarName = name.substring(0, name.length() - JarFileSystem.JAR_SEPARATOR.length());
741                   return jarName.substring(jarName.lastIndexOf('/') + 1);
742                 }
743
744                 return name;
745               }
746             };
747           }
748           else {
749             // fake root for windows
750             root = new VirtualDirectoryImpl("", null, fs, rootId) {
751               @Override
752               @NotNull
753               public VirtualFile[] getChildren() {
754                 return getRoots(fs);
755               }
756
757               @Override
758               public VirtualFileSystemEntry findChild(@NotNull String name) {
759                 if (name.length() == 0) return null;
760                 return findRoot(name, fs);
761               }
762             };
763           }
764           if (!fs.exists(root)) return null;
765
766           boolean newRoot = copyRecordFromDelegateFS(rootId, 0, root, fs);
767           if (!newRoot) {
768             if (fs.getTimeStamp(root) != FSRecords.getTimestamp(rootId)) {
769               root.markDirtyRecursively();
770             }
771           }
772         }
773         catch (IOException e) {
774           throw new RuntimeException(e);
775         }
776
777         if (basePath.length() > 0) {
778           myRoots.put(rootUrl, root);
779           myRootsById.put(root.getId(), root);
780         }
781         else {
782           myFakeRoot = root;
783         }
784       }
785
786       return root;
787     }
788   }
789
790   @Override
791   public void refresh(final boolean asynchronous) {
792     final NewVirtualFile[] roots;
793     synchronized (LOCK) {
794       Collection<VirtualFileSystemEntry> values = myRoots.values();
795       roots = values.toArray(new NewVirtualFile[values.size()]);
796     }
797
798     RefreshQueue.getInstance().refresh(asynchronous, true, null, roots);
799   }
800
801   @Override
802   public void refresh(boolean asynchronous, Runnable postAction, ModalityState modalityState) {
803     final NewVirtualFile[] roots;
804     synchronized (LOCK) {
805       Collection<VirtualFileSystemEntry> values = myRoots.values();
806       roots = values.toArray(new NewVirtualFile[values.size()]);
807     }
808
809     RefreshQueue.getInstance().refresh(asynchronous, true, postAction, modalityState, roots);
810   }
811
812   @Override
813   @NotNull
814   public VirtualFile[] getLocalRoots() {
815     List<NewVirtualFile> roots;
816     synchronized (LOCK) {
817       roots = new ArrayList<NewVirtualFile>(myRoots.values());
818
819       final Iterator<NewVirtualFile> it = roots.iterator();
820       while (it.hasNext()) {
821         NewVirtualFile file = it.next();
822         if (!file.isInLocalFileSystem()) {
823           it.remove();
824         }
825       }
826     }
827
828     return VfsUtil.toVirtualFileArray(roots);
829   }
830
831   //guarded by dirCacheReadLock/dirCacheWriteLock
832   private final StripedLockIntObjectConcurrentHashMap<NewVirtualFile> myIdToDirCache = new StripedLockIntObjectConcurrentHashMap<NewVirtualFile>();
833
834   public void clearIdCache() {
835     myIdToDirCache.clear();
836   }
837
838   @Override
839   @Nullable
840   public NewVirtualFile findFileById(final int id) {
841     return _findFileById(id, false);
842   }
843
844
845   @Nullable
846   public NewVirtualFile findFileByIdIfCached(final int id) {
847     return _findFileById(id, true);
848   }
849
850   @Nullable
851   private NewVirtualFile _findFileById(int id, final boolean cachedOnly) {
852     final NewVirtualFile cached = myIdToDirCache.get(id);
853     if (cached != null) {
854       return cached;
855     }
856
857     NewVirtualFile result = doFindFile(id, cachedOnly);
858
859     if (result != null && result.isDirectory()) {
860       NewVirtualFile old = myIdToDirCache.putIfAbsent(id, result);
861       if (old != null) result = old;
862     }
863     return result;
864   }
865
866   @Nullable
867   private NewVirtualFile doFindFile(final int id, boolean cachedOnly) {
868     final int parentId = getParent(id);
869     if (parentId == 0) {
870       synchronized (LOCK) {
871         return myRootsById.get(id);
872       }
873     }
874     else {
875       NewVirtualFile parentFile = _findFileById(parentId, cachedOnly);
876       if (parentFile == null) {
877         return null;
878       }
879       return cachedOnly ? parentFile.findChildByIdIfCached(id) : parentFile.findChildById(id);
880     }
881   }
882
883   @Override
884   @NotNull
885   public VirtualFile[] getRoots() {
886     synchronized (LOCK) {
887       Collection<VirtualFileSystemEntry> roots = myRoots.values();
888       return VfsUtil.toVirtualFileArray(roots);
889     }
890   }
891
892   @Override
893   @NotNull
894   public VirtualFile[] getRoots(@NotNull final NewVirtualFileSystem fs) {
895     List<VirtualFile> roots = new ArrayList<VirtualFile>();
896     synchronized (LOCK) {
897       for (NewVirtualFile root : myRoots.values()) {
898         if (root.getFileSystem() == fs) {
899           roots.add(root);
900         }
901       }
902     }
903
904     return VfsUtil.toVirtualFileArray(roots);
905   }
906
907   private void applyEvent(final VFileEvent event) {
908     try {
909       if (event instanceof VFileCreateEvent) {
910         final VFileCreateEvent createEvent = (VFileCreateEvent)event;
911         executeCreateChild(createEvent.getParent(), createEvent.getChildName());
912       }
913       else if (event instanceof VFileDeleteEvent) {
914         final VFileDeleteEvent deleteEvent = (VFileDeleteEvent)event;
915         executeDelete(deleteEvent.getFile());
916       }
917       else if (event instanceof VFileContentChangeEvent) {
918         final VFileContentChangeEvent contentUpdateEvent = (VFileContentChangeEvent)event;
919         executeTouch(contentUpdateEvent.getFile(), contentUpdateEvent.isFromRefresh(), contentUpdateEvent.getModificationStamp());
920       }
921       else if (event instanceof VFileCopyEvent) {
922         final VFileCopyEvent copyEvent = (VFileCopyEvent)event;
923         executeCopy(copyEvent.getFile(), copyEvent.getNewParent(), copyEvent.getNewChildName());
924       }
925       else if (event instanceof VFileMoveEvent) {
926         final VFileMoveEvent moveEvent = (VFileMoveEvent)event;
927         executeMove(moveEvent.getFile(), moveEvent.getNewParent());
928       }
929       else if (event instanceof VFilePropertyChangeEvent) {
930         final VFilePropertyChangeEvent propertyChangeEvent = (VFilePropertyChangeEvent)event;
931         if (VirtualFile.PROP_NAME.equals(propertyChangeEvent.getPropertyName())) {
932           executeRename(propertyChangeEvent.getFile(), (String)propertyChangeEvent.getNewValue());
933         }
934         else if (VirtualFile.PROP_WRITABLE.equals(propertyChangeEvent.getPropertyName())) {
935           executeSetWritable(propertyChangeEvent.getFile(), ((Boolean)propertyChangeEvent.getNewValue()).booleanValue());
936         }
937       }
938     }
939     catch (Exception e) {
940       // Exception applying single event should not prevent other events from applying.
941       LOG.error(e);
942     }
943   }
944
945   @NonNls
946   public String toString() {
947     return "PersistentFS";
948   }
949
950   private static void executeCreateChild(final VirtualFile parent, final String name) {
951     final NewVirtualFileSystem delegate = getDelegate(parent);
952     VirtualFile fakeFile = new FakeVirtualFile(parent, name);
953     if (delegate.exists(fakeFile)) {
954       final int parentId = getFileId(parent);
955       int childId = createAndCopyRecord(delegate, fakeFile, parentId);
956
957       appendIdToParentList(parentId, childId);
958       final VirtualDirectoryImpl dir = (VirtualDirectoryImpl)parent;
959       dir.addChild(dir.createChild(name, childId));
960     }
961   }
962
963   private static int createAndCopyRecord(NewVirtualFileSystem delegateSystem, VirtualFile delegateFile, int parentId) {
964     int childId = FSRecords.createRecord();
965     copyRecordFromDelegateFS(childId, parentId, delegateFile, delegateSystem);
966     return childId;
967   }
968
969   private static void appendIdToParentList(final int parentId, final int childId) {
970     int[] childrenList = FSRecords.list(parentId);
971     childrenList = ArrayUtil.append(childrenList, childId);
972     FSRecords.updateList(parentId, childrenList);
973   }
974
975   private void executeDelete(final VirtualFile file) {
976     if (!file.exists()) {
977       LOG.error("Deleting a file, which does not exist: " + file.getPath());
978     }
979     else {
980       clearIdCache();
981
982       final int id = getFileId(file);
983
984       final VirtualFile parent = file.getParent();
985       final int parentId = parent != null ? getFileId(parent) : 0;
986
987       FSRecords.deleteRecordRecursively(id);
988
989       if (parentId != 0) {
990         removeIdFromParentList(parentId, id, parent, file);
991         VirtualDirectoryImpl directory = (VirtualDirectoryImpl)file.getParent();
992         assert directory != null;
993
994         directory.removeChild(file);
995       }
996       else {
997         synchronized (LOCK) {
998           myRoots.remove(file.getUrl());
999           myRootsById.remove(id);
1000           try {
1001             FSRecords.deleteRootRecord(id);
1002           }
1003           catch (IOException e) {
1004             throw new RuntimeException(e);
1005           }
1006         }
1007       }
1008
1009       invalidateSubtree(file);
1010     }
1011   }
1012
1013   private static void invalidateSubtree(final VirtualFile file) {
1014     final VirtualFileSystemEntry impl = (VirtualFileSystemEntry)file;
1015     impl.invalidate();
1016     for (VirtualFile child : impl.getCachedChildren()) {
1017       invalidateSubtree(child);
1018     }
1019   }
1020
1021   private static void removeIdFromParentList(final int parentId, final int id, VirtualFile parent, VirtualFile file) {
1022     int[] childList = FSRecords.list(parentId);
1023
1024     int index = ArrayUtil.indexOf(childList, id);
1025     if (index == -1) {
1026       throw new RuntimeException("Cannot find child (" + id + ")" + file
1027                                  + "\n\tin (" + parentId + ")" + parent
1028                                  + "\n\tactual children:" + Arrays.toString(childList));
1029     }
1030     childList = ArrayUtil.remove(childList, index);
1031     FSRecords.updateList(parentId, childList);
1032   }
1033
1034   private static void executeRename(final VirtualFile file, final String newName) {
1035     ((VirtualFileSystemEntry)file).setName(newName);
1036     final int id = getFileId(file);
1037     FSRecords.setName(id, newName);
1038   }
1039
1040   private static void executeSetWritable(final VirtualFile file, final boolean writableFlag) {
1041     setFlag(file, IS_READ_ONLY, !writableFlag);
1042   }
1043
1044   private static void setFlag(VirtualFile file, int mask, boolean value) {
1045     setFlag(getFileId(file), mask, value);
1046   }
1047
1048   private static void setFlag(final int id, final int mask, final boolean value) {
1049     int oldFlags = FSRecords.getFlags(id);
1050     int flags = value ? oldFlags | mask : oldFlags & ~mask;
1051
1052     if (oldFlags != flags) {
1053       FSRecords.setFlags(id, flags, true);
1054     }
1055   }
1056
1057   private static boolean checkFlag(VirtualFile file, int mask) {
1058     return (FSRecords.getFlags(getFileId(file)) & mask) != 0;
1059   }
1060
1061   private static void executeTouch(final VirtualFile file, boolean reloadContentFromDelegate, long newModificationStamp) {
1062     if (reloadContentFromDelegate) {
1063       setFlag(file, MUST_RELOAD_CONTENT, true);
1064     }
1065
1066     final NewVirtualFileSystem delegate = getDelegate(file);
1067     FSRecords.setLength(getFileId(file), delegate.getLength(file));
1068     FSRecords.setTimestamp(getFileId(file), delegate.getTimeStamp(file));
1069
1070     ((NewVirtualFile)file).setModificationStamp(newModificationStamp);
1071   }
1072
1073   @SuppressWarnings({"UnusedDeclaration"})
1074   private static void executeCopy(final VirtualFile from, final VirtualFile newParent, final String copyName) {
1075     executeCreateChild(newParent, copyName);
1076   }
1077
1078   private static void executeMove(final VirtualFile file, final VirtualFile newParent) {
1079     final int fileId = getFileId(file);
1080     final int newParentId = getFileId(newParent);
1081     final int oldParentId = getFileId(file.getParent());
1082
1083     removeIdFromParentList(oldParentId, fileId, file.getParent(), file);
1084     appendIdToParentList(newParentId, fileId);
1085
1086     ((VirtualFileSystemEntry)file).setParent(newParent);
1087     FSRecords.setParent(fileId, newParentId);
1088   }
1089
1090   public String getName(final int id) {
1091     assert id > 0;
1092     return FSRecords.getName(id);
1093   }
1094
1095   public void cleanPersistedContents() {
1096     try {
1097       final int[] roots = FSRecords.listRoots();
1098       for (int root : roots) {
1099         cleanPersistedContentsRecursively(root);
1100       }
1101     }
1102     catch (IOException e) {
1103       throw new RuntimeException(e);
1104     }
1105   }
1106
1107   private static void cleanPersistedContentsRecursively(int id) {
1108     if (isDirectory(id)) {
1109       for (int child : FSRecords.list(id)) {
1110         cleanPersistedContentsRecursively(child);
1111       }
1112     }
1113     else {
1114       setFlag(id, MUST_RELOAD_CONTENT, true);
1115     }
1116   }
1117
1118   public static NewVirtualFileSystem replaceWithNativeFS(NewVirtualFileSystem delegate) {
1119     if (delegate.getProtocol().equals(LocalFileSystem.PROTOCOL) && Registry.is("filesystem.useNative")) {
1120         if (SystemInfo.isWindows && Win32LocalFileSystem.isAvailable()) {
1121           delegate = Win32LocalFileSystem.getWin32Instance();
1122         }
1123       }
1124     return delegate;
1125   }
1126 }