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