3f963b50f1da84556a6f686b0899d3d3fce2156d
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / vfs / newvfs / persistent / PersistentFSImpl.java
1 /*
2  * Copyright 2000-2016 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.intellij.concurrency.JobSchedulerImpl;
19 import com.intellij.openapi.application.Application;
20 import com.intellij.openapi.application.ApplicationManager;
21 import com.intellij.openapi.components.ApplicationComponent;
22 import com.intellij.openapi.diagnostic.Logger;
23 import com.intellij.openapi.util.Comparing;
24 import com.intellij.openapi.util.LowMemoryWatcher;
25 import com.intellij.openapi.util.ShutDownTracker;
26 import com.intellij.openapi.util.io.*;
27 import com.intellij.openapi.util.text.StringUtil;
28 import com.intellij.openapi.vfs.*;
29 import com.intellij.openapi.vfs.ex.temp.TempFileSystem;
30 import com.intellij.openapi.vfs.newvfs.*;
31 import com.intellij.openapi.vfs.newvfs.events.*;
32 import com.intellij.openapi.vfs.newvfs.impl.*;
33 import com.intellij.util.*;
34 import com.intellij.util.containers.ConcurrentIntObjectMap;
35 import com.intellij.util.containers.ContainerUtil;
36 import com.intellij.util.containers.EmptyIntHashSet;
37 import com.intellij.util.io.ReplicatorInputStream;
38 import com.intellij.util.io.URLUtil;
39 import com.intellij.util.messages.MessageBus;
40 import gnu.trove.*;
41 import org.jetbrains.annotations.NonNls;
42 import org.jetbrains.annotations.NotNull;
43 import org.jetbrains.annotations.Nullable;
44 import org.jetbrains.annotations.TestOnly;
45
46 import java.io.*;
47 import java.util.*;
48 import java.util.concurrent.atomic.AtomicBoolean;
49
50 /**
51  * @author max
52  */
53 public class PersistentFSImpl extends PersistentFS implements ApplicationComponent {
54   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vfs.newvfs.persistent.PersistentFS");
55
56   private final MessageBus myEventBus;
57
58   private final Map<String, VirtualFileSystemEntry> myRoots = ContainerUtil.newConcurrentMap(10, 0.4f, JobSchedulerImpl.CORES_COUNT, FileUtil.PATH_HASHING_STRATEGY);
59   private final ConcurrentIntObjectMap<VirtualFileSystemEntry> myRootsById = ContainerUtil.createConcurrentIntObjectMap(10, 0.4f, JobSchedulerImpl.CORES_COUNT);
60
61   // FS roots must be in this map too. findFileById() relies on this.
62   private final ConcurrentIntObjectMap<VirtualFileSystemEntry> myIdToDirCache = ContainerUtil.createConcurrentIntObjectMap();
63   private final Object myInputLock = new Object();
64
65   private final AtomicBoolean myShutDown = new AtomicBoolean(false);
66   @SuppressWarnings({"FieldCanBeLocal", "unused"})
67   private final LowMemoryWatcher myWatcher = LowMemoryWatcher.register(() -> clearIdCache());
68   private volatile int myStructureModificationCount;
69
70   public PersistentFSImpl(@NotNull MessageBus bus) {
71     myEventBus = bus;
72     ShutDownTracker.getInstance().registerShutdownTask(() -> performShutdown());
73   }
74
75   @Override
76   public void initComponent() {
77     FSRecords.connect();
78   }
79
80   @Override
81   public void disposeComponent() {
82     performShutdown();
83   }
84
85   private void performShutdown() {
86     if (myShutDown.compareAndSet(false, true)) {
87       LOG.info("VFS dispose started");
88       FSRecords.dispose();
89       LOG.info("VFS dispose completed");
90     }
91   }
92
93   @Override
94   @NonNls
95   @NotNull
96   public String getComponentName() {
97     return "app.component.PersistentFS";
98   }
99
100   @Override
101   public boolean areChildrenLoaded(@NotNull final VirtualFile dir) {
102     return areChildrenLoaded(getFileId(dir));
103   }
104
105   @Override
106   public long getCreationTimestamp() {
107     return FSRecords.getCreationTimestamp();
108   }
109
110   @NotNull
111   private static NewVirtualFileSystem getDelegate(@NotNull VirtualFile file) {
112     return (NewVirtualFileSystem)file.getFileSystem();
113   }
114
115   @Override
116   public boolean wereChildrenAccessed(@NotNull final VirtualFile dir) {
117     return FSRecords.wereChildrenAccessed(getFileId(dir));
118   }
119
120   @Override
121   @NotNull
122   public String[] list(@NotNull final VirtualFile file) {
123     int id = getFileId(file);
124
125     FSRecords.NameId[] nameIds = FSRecords.listAll(id);
126     if (!areChildrenLoaded(id)) {
127       nameIds  = persistAllChildren(file, id, nameIds);
128     }
129     return ContainerUtil.map2Array(nameIds, String.class, id1 -> id1.name.toString());
130   }
131
132   @Override
133   @NotNull
134   public String[] listPersisted(@NotNull VirtualFile parent) {
135     return listPersisted(FSRecords.list(getFileId(parent)));
136   }
137
138   @NotNull
139   private static String[] listPersisted(@NotNull int[] childrenIds) {
140     String[] names = ArrayUtil.newStringArray(childrenIds.length);
141     for (int i = 0; i < childrenIds.length; i++) {
142       names[i] = FSRecords.getName(childrenIds[i]);
143     }
144     return names;
145   }
146
147   @NotNull
148   private static FSRecords.NameId[] persistAllChildren(@NotNull final VirtualFile file, final int id, @NotNull FSRecords.NameId[] current) {
149     final NewVirtualFileSystem fs = replaceWithNativeFS(getDelegate(file));
150
151     String[] delegateNames = VfsUtil.filterNames(fs.list(file));
152     if (delegateNames.length == 0 && current.length > 0) {
153       return current;
154     }
155
156     Set<String> toAdd = ContainerUtil.newHashSet(delegateNames);
157     for (FSRecords.NameId nameId : current) {
158       toAdd.remove(nameId.name.toString());
159     }
160
161     final TIntArrayList childrenIds = new TIntArrayList(current.length + toAdd.size());
162     final List<FSRecords.NameId> nameIds = ContainerUtil.newArrayListWithCapacity(current.length + toAdd.size());
163     for (FSRecords.NameId nameId : current) {
164       childrenIds.add(nameId.id);
165       nameIds.add(nameId);
166     }
167     for (String newName : toAdd) {
168       FakeVirtualFile child = new FakeVirtualFile(file, newName);
169       FileAttributes attributes = fs.getAttributes(child);
170       if (attributes != null) {
171         int childId = createAndFillRecord(fs, child, id, attributes);
172         childrenIds.add(childId);
173         nameIds.add(new FSRecords.NameId(childId, FileNameCache.storeName(newName), newName));
174       }
175     }
176
177     FSRecords.updateList(id, childrenIds.toNativeArray());
178     setChildrenCached(id);
179
180     return nameIds.toArray(new FSRecords.NameId[nameIds.size()]);
181   }
182
183   private static void setChildrenCached(int id) {
184     int flags = FSRecords.getFlags(id);
185     FSRecords.setFlags(id, flags | CHILDREN_CACHED_FLAG, true);
186   }
187
188   @Override
189   @NotNull
190   public FSRecords.NameId[] listAll(@NotNull VirtualFile parent) {
191     final int parentId = getFileId(parent);
192
193     FSRecords.NameId[] nameIds = FSRecords.listAll(parentId);
194     if (!areChildrenLoaded(parentId)) {
195       return persistAllChildren(parent, parentId, nameIds);
196     }
197
198     return nameIds;
199   }
200
201   private static boolean areChildrenLoaded(final int parentId) {
202     return BitUtil.isSet(FSRecords.getFlags(parentId), CHILDREN_CACHED_FLAG);
203   }
204
205   @Override
206   @Nullable
207   public DataInputStream readAttribute(@NotNull final VirtualFile file, @NotNull final FileAttribute att) {
208     return FSRecords.readAttributeWithLock(getFileId(file), att);
209   }
210
211   @Override
212   @NotNull
213   public DataOutputStream writeAttribute(@NotNull final VirtualFile file, @NotNull final FileAttribute att) {
214     return FSRecords.writeAttribute(getFileId(file), att);
215   }
216
217   @Nullable
218   private static DataInputStream readContent(@NotNull VirtualFile file) {
219     return FSRecords.readContent(getFileId(file));
220   }
221
222   @Nullable
223   private static DataInputStream readContentById(int contentId) {
224     return FSRecords.readContentById(contentId);
225   }
226
227   @NotNull
228   private static DataOutputStream writeContent(@NotNull VirtualFile file, boolean readOnly) {
229     return FSRecords.writeContent(getFileId(file), readOnly);
230   }
231
232   private static void writeContent(@NotNull VirtualFile file, ByteSequence content, boolean readOnly) throws IOException {
233     FSRecords.writeContent(getFileId(file), content, readOnly);
234   }
235
236   @Override
237   public int storeUnlinkedContent(@NotNull byte[] bytes) {
238     return FSRecords.storeUnlinkedContent(bytes);
239   }
240
241   @Override
242   public int getModificationCount(@NotNull final VirtualFile file) {
243     return FSRecords.getModCount(getFileId(file));
244   }
245
246   @Override
247   public int getModificationCount() {
248     return FSRecords.getLocalModCount();
249   }
250
251   @Override
252   public int getStructureModificationCount() {
253     return myStructureModificationCount;
254   }
255
256   public void incStructuralModificationCount() {
257     myStructureModificationCount++;
258   }
259
260   @Override
261   public int getFilesystemModificationCount() {
262     return FSRecords.getModCount();
263   }
264
265   private static boolean writeAttributesToRecord(final int id,
266                                                  final int parentId,
267                                                  @NotNull VirtualFile file,
268                                                  @NotNull NewVirtualFileSystem fs,
269                                                  @NotNull FileAttributes attributes) {
270     String name = file.getName();
271     if (!name.isEmpty()) {
272       if (namesEqual(fs, name, FSRecords.getNameSequence(id))) return false; // TODO: Handle root attributes change.
273     }
274     else {
275       if (areChildrenLoaded(id)) return false; // TODO: hack
276     }
277
278     FSRecords.writeAttributesToRecord(id, parentId, attributes, name);
279
280     return true;
281   }
282
283   @Override
284   public int getFileAttributes(int id) {
285     assert id > 0;
286     //noinspection MagicConstant
287     return FSRecords.getFlags(id);
288   }
289
290   @Override
291   public boolean isDirectory(@NotNull final VirtualFile file) {
292     return isDirectory(getFileAttributes(getFileId(file)));
293   }
294
295   private static boolean namesEqual(@NotNull VirtualFileSystem fs, @NotNull CharSequence n1, CharSequence n2) {
296     return Comparing.equal(n1, n2, fs.isCaseSensitive());
297   }
298
299   @Override
300   public boolean exists(@NotNull final VirtualFile fileOrDirectory) {
301     return ((VirtualFileWithId)fileOrDirectory).getId() > 0;
302   }
303
304   @Override
305   public long getTimeStamp(@NotNull final VirtualFile file) {
306     return FSRecords.getTimestamp(getFileId(file));
307   }
308
309   @Override
310   public void setTimeStamp(@NotNull final VirtualFile file, final long modStamp) throws IOException {
311     final int id = getFileId(file);
312     FSRecords.setTimestamp(id, modStamp);
313     getDelegate(file).setTimeStamp(file, modStamp);
314   }
315
316   private static int getFileId(@NotNull VirtualFile file) {
317     final int id = ((VirtualFileWithId)file).getId();
318     if (id <= 0) {
319       throw new InvalidVirtualFileAccessException(file);
320     }
321     return id;
322   }
323
324   @Override
325   public boolean isSymLink(@NotNull VirtualFile file) {
326     return isSymLink(getFileAttributes(getFileId(file)));
327   }
328
329   @Override
330   public String resolveSymLink(@NotNull VirtualFile file) {
331     throw new UnsupportedOperationException();
332   }
333
334   @Override
335   public boolean isWritable(@NotNull VirtualFile file) {
336     return !BitUtil.isSet(getFileAttributes(getFileId(file)), IS_READ_ONLY);
337   }
338
339   @Override
340   public boolean isHidden(@NotNull VirtualFile file) {
341     return BitUtil.isSet(getFileAttributes(getFileId(file)), IS_HIDDEN);
342   }
343
344   @Override
345   public void setWritable(@NotNull final VirtualFile file, final boolean writableFlag) throws IOException {
346     getDelegate(file).setWritable(file, writableFlag);
347     boolean oldWritable = isWritable(file);
348     if (oldWritable != writableFlag) {
349       processEvent(new VFilePropertyChangeEvent(this, file, VirtualFile.PROP_WRITABLE, oldWritable, writableFlag, false));
350     }
351   }
352
353   @Override
354   public int getId(@NotNull VirtualFile parent, @NotNull String childName, @NotNull NewVirtualFileSystem fs) {
355     int parentId = getFileId(parent);
356     int[] children = FSRecords.list(parentId);
357
358     if (children.length > 0) {
359       // fast path, check that some child has same nameId as given name, this avoid O(N) on retrieving names for processing non-cached children
360       int nameId = FSRecords.getNameId(childName);
361       for (final int childId : children) {
362         if (nameId == FSRecords.getNameId(childId)) {
363           return childId;
364         }
365       }
366       // for case sensitive system the above check is exhaustive in consistent state of vfs
367     }
368
369     for (final int childId : children) {
370       if (namesEqual(fs, childName, FSRecords.getNameSequence(childId))) return childId;
371     }
372
373     final VirtualFile fake = new FakeVirtualFile(parent, childName);
374     final FileAttributes attributes = fs.getAttributes(fake);
375     if (attributes != null) {
376       final int child = createAndFillRecord(fs, fake, parentId, attributes);
377       FSRecords.updateList(parentId, ArrayUtil.append(children, child));
378       return child;
379     }
380
381     return 0;
382   }
383
384   @Override
385   public long getLength(@NotNull VirtualFile file) {
386     long len;
387     if (mustReloadContent(file)) {
388       len = reloadLengthFromDelegate(file, getDelegate(file));
389     }
390     else {
391       len = getLastRecordedLength(file);
392     }
393
394     return len;
395   }
396
397   @Override
398   public long getLastRecordedLength(@NotNull VirtualFile file) {
399     int id = getFileId(file);
400     return FSRecords.getLength(id);
401   }
402
403   @NotNull
404   @Override
405   public VirtualFile copyFile(Object requestor, @NotNull VirtualFile file, @NotNull VirtualFile parent, @NotNull String name) throws IOException {
406     getDelegate(file).copyFile(requestor, file, parent, name);
407     processEvent(new VFileCopyEvent(requestor, file, parent, name));
408
409     final VirtualFile child = parent.findChild(name);
410     if (child == null) {
411       throw new IOException("Cannot create child");
412     }
413     return child;
414   }
415
416   @NotNull
417   @Override
418   public VirtualFile createChildDirectory(Object requestor, @NotNull VirtualFile parent, @NotNull String dir) throws IOException {
419     getDelegate(parent).createChildDirectory(requestor, parent, dir);
420     processEvent(new VFileCreateEvent(requestor, parent, dir, true, false));
421
422     final VirtualFile child = parent.findChild(dir);
423     if (child == null) {
424       throw new IOException("Cannot create child directory '" + dir + "' at " + parent.getPath());
425     }
426     return child;
427   }
428
429   @NotNull
430   @Override
431   public VirtualFile createChildFile(Object requestor, @NotNull VirtualFile parent, @NotNull String file) throws IOException {
432     getDelegate(parent).createChildFile(requestor, parent, file);
433     processEvent(new VFileCreateEvent(requestor, parent, file, false, false));
434
435     final VirtualFile child = parent.findChild(file);
436     if (child == null) {
437       throw new IOException("Cannot create child file '" + file + "' at " + parent.getPath());
438     }
439     return child;
440   }
441
442   @Override
443   public void deleteFile(final Object requestor, @NotNull final VirtualFile file) throws IOException {
444     final NewVirtualFileSystem delegate = getDelegate(file);
445     delegate.deleteFile(requestor, file);
446
447     if (!delegate.exists(file)) {
448       processEvent(new VFileDeleteEvent(requestor, file, false));
449     }
450   }
451
452   @Override
453   public void renameFile(final Object requestor, @NotNull VirtualFile file, @NotNull String newName) throws IOException {
454     getDelegate(file).renameFile(requestor, file, newName);
455     String oldName = file.getName();
456     if (!newName.equals(oldName)) {
457       processEvent(new VFilePropertyChangeEvent(requestor, file, VirtualFile.PROP_NAME, oldName, newName, false));
458     }
459   }
460
461   @Override
462   @NotNull
463   public byte[] contentsToByteArray(@NotNull final VirtualFile file) throws IOException {
464     return contentsToByteArray(file, true);
465   }
466
467   @Override
468   @NotNull
469   public byte[] contentsToByteArray(@NotNull final VirtualFile file, boolean cacheContent) throws IOException {
470     InputStream contentStream = null;
471     boolean reloadFromDelegate;
472     boolean outdated;
473     int fileId;
474     long length = -1L;
475
476     synchronized (myInputLock) {
477       fileId = getFileId(file);
478       outdated = checkFlag(fileId, MUST_RELOAD_CONTENT) || (length = FSRecords.getLength(fileId)) == -1L;
479       reloadFromDelegate = outdated || (contentStream = readContent(file)) == null;
480     }
481
482     if (reloadFromDelegate) {
483       final NewVirtualFileSystem delegate = getDelegate(file);
484
485       final byte[] content;
486       if (outdated) {
487         // in this case, file can have out-of-date length. so, update it first (it's needed for correct contentsToByteArray() work)
488         // see IDEA-90813 for possible bugs
489         FSRecords.setLength(fileId, delegate.getLength(file));
490         content = delegate.contentsToByteArray(file);
491       }
492       else {
493         // a bit of optimization
494         content = delegate.contentsToByteArray(file);
495         FSRecords.setLength(fileId, content.length);
496       }
497
498       Application application = ApplicationManager.getApplication();
499       // we should cache every local files content
500       // because the local history feature is currently depends on this cache,
501       // perforce offline mode as well
502       if ((!delegate.isReadOnly() ||
503            // do not cache archive content unless asked
504            cacheContent && !application.isInternal() && !application.isUnitTestMode()) &&
505           content.length <= PersistentFSConstants.FILE_LENGTH_TO_CACHE_THRESHOLD) {
506         synchronized (myInputLock) {
507           writeContent(file, new ByteSequence(content), delegate.isReadOnly());
508           setFlag(file, MUST_RELOAD_CONTENT, false);
509         }
510       }
511
512       return content;
513     }
514     else {
515       try {
516         assert length >= 0 : file;
517         return FileUtil.loadBytes(contentStream, (int)length);
518       }
519       catch (IOException e) {
520         throw FSRecords.handleError(e);
521       }
522     }
523   }
524
525   @Override
526   @NotNull
527   public byte[] contentsToByteArray(int contentId) throws IOException {
528     final DataInputStream stream = readContentById(contentId);
529     assert stream != null : contentId;
530     return FileUtil.loadBytes(stream);
531   }
532
533   @Override
534   @NotNull
535   public InputStream getInputStream(@NotNull final VirtualFile file) throws IOException {
536     synchronized (myInputLock) {
537       InputStream contentStream;
538       if (mustReloadContent(file) || (contentStream = readContent(file)) == null) {
539         NewVirtualFileSystem delegate = getDelegate(file);
540         long len = reloadLengthFromDelegate(file, delegate);
541         InputStream nativeStream = delegate.getInputStream(file);
542
543         if (len > PersistentFSConstants.FILE_LENGTH_TO_CACHE_THRESHOLD) return nativeStream;
544         return createReplicator(file, nativeStream, len, delegate.isReadOnly());
545       }
546       else {
547         return contentStream;
548       }
549     }
550   }
551
552   private static long reloadLengthFromDelegate(@NotNull VirtualFile file, @NotNull NewVirtualFileSystem delegate) {
553     final long len = delegate.getLength(file);
554     FSRecords.setLength(getFileId(file), len);
555     return len;
556   }
557
558   private InputStream createReplicator(@NotNull final VirtualFile file,
559                                        final InputStream nativeStream,
560                                        final long fileLength,
561                                        final boolean readOnly) throws IOException {
562     if (nativeStream instanceof BufferExposingByteArrayInputStream) {
563       // optimization
564       BufferExposingByteArrayInputStream  byteStream = (BufferExposingByteArrayInputStream )nativeStream;
565       byte[] bytes = byteStream.getInternalBuffer();
566       storeContentToStorage(fileLength, file, readOnly, bytes, bytes.length);
567       return nativeStream;
568     }
569     @SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
570     final BufferExposingByteArrayOutputStream cache = new BufferExposingByteArrayOutputStream((int)fileLength);
571     return new ReplicatorInputStream(nativeStream, cache) {
572       @Override
573       public void close() throws IOException {
574         super.close();
575         storeContentToStorage(fileLength, file, readOnly, cache.getInternalBuffer(), cache.size());
576       }
577     };
578   }
579
580   private void storeContentToStorage(long fileLength,
581                                      @NotNull VirtualFile file,
582                                      boolean readOnly, @NotNull byte[] bytes, int bytesLength)
583     throws IOException {
584     synchronized (myInputLock) {
585       if (bytesLength == fileLength) {
586         writeContent(file, new ByteSequence(bytes, 0, bytesLength), readOnly);
587         setFlag(file, MUST_RELOAD_CONTENT, false);
588       }
589       else {
590         setFlag(file, MUST_RELOAD_CONTENT, true);
591       }
592     }
593   }
594
595   private static boolean mustReloadContent(@NotNull VirtualFile file) {
596     int fileId = getFileId(file);
597     return checkFlag(fileId, MUST_RELOAD_CONTENT) || FSRecords.getLength(fileId) == -1L;
598   }
599
600   @Override
601   @NotNull
602   public OutputStream getOutputStream(@NotNull final VirtualFile file,
603                                       final Object requestor,
604                                       final long modStamp,
605                                       final long timeStamp) throws IOException {
606     return new ByteArrayOutputStream() {
607       private boolean closed; // protection against user calling .close() twice
608
609       @Override
610       public void close() throws IOException {
611         if (closed) return;
612         super.close();
613
614         ApplicationManager.getApplication().assertWriteAccessAllowed();
615
616         VFileContentChangeEvent event = new VFileContentChangeEvent(requestor, file, file.getModificationStamp(), modStamp, false);
617         List<VFileContentChangeEvent> events = Collections.singletonList(event);
618         BulkFileListener publisher = myEventBus.syncPublisher(VirtualFileManager.VFS_CHANGES);
619         publisher.before(events);
620
621         NewVirtualFileSystem delegate = getDelegate(file);
622         OutputStream ioFileStream = delegate.getOutputStream(file, requestor, modStamp, timeStamp);
623         // FSRecords.ContentOutputStream already buffered, no need to wrap in BufferedStream
624         OutputStream persistenceStream = writeContent(file, delegate.isReadOnly());
625
626         try {
627           persistenceStream.write(buf, 0, count);
628         }
629         finally {
630           try {
631             ioFileStream.write(buf, 0, count);
632           }
633           finally {
634             closed = true;
635             persistenceStream.close();
636             ioFileStream.close();
637
638             executeTouch(file, false, event.getModificationStamp());
639             publisher.after(events);
640           }
641         }
642       }
643     };
644   }
645
646   @Override
647   public int acquireContent(@NotNull VirtualFile file) {
648     return FSRecords.acquireFileContent(getFileId(file));
649   }
650
651   @Override
652   public void releaseContent(int contentId) {
653     FSRecords.releaseContent(contentId);
654   }
655
656   @Override
657   public int getCurrentContentId(@NotNull VirtualFile file) {
658     return FSRecords.getContentId(getFileId(file));
659   }
660
661   @Override
662   public void moveFile(final Object requestor, @NotNull final VirtualFile file, @NotNull final VirtualFile newParent) throws IOException {
663     getDelegate(file).moveFile(requestor, file, newParent);
664     processEvent(new VFileMoveEvent(requestor, file, newParent));
665   }
666
667   private void processEvent(@NotNull VFileEvent event) {
668     processEvents(Collections.singletonList(event));
669   }
670
671   private static class EventWrapper {
672     private final VFileDeleteEvent event;
673     private final int id;
674
675     private EventWrapper(final VFileDeleteEvent event, final int id) {
676       this.event = event;
677       this.id = id;
678     }
679   }
680
681   @NotNull private static final Comparator<EventWrapper> DEPTH_COMPARATOR = (o1, o2) -> o1.event.getFileDepth() - o2.event.getFileDepth();
682
683   @NotNull
684   private static List<VFileEvent> validateEvents(@NotNull List<VFileEvent> events) {
685     final List<EventWrapper> deletionEvents = ContainerUtil.newArrayList();
686     for (int i = 0, size = events.size(); i < size; i++) {
687       final VFileEvent event = events.get(i);
688       if (event instanceof VFileDeleteEvent && event.isValid()) {
689         deletionEvents.add(new EventWrapper((VFileDeleteEvent)event, i));
690       }
691     }
692
693     final TIntHashSet invalidIDs;
694     if (deletionEvents.isEmpty()) {
695       invalidIDs = EmptyIntHashSet.INSTANCE;
696     }
697     else {
698       ContainerUtil.quickSort(deletionEvents, DEPTH_COMPARATOR);
699
700       invalidIDs = new TIntHashSet(deletionEvents.size());
701       final Set<VirtualFile> dirsToBeDeleted = new THashSet<>(deletionEvents.size());
702       nextEvent:
703       for (EventWrapper wrapper : deletionEvents) {
704         final VirtualFile candidate = wrapper.event.getFile();
705         VirtualFile parent = candidate;
706         while (parent != null) {
707           if (dirsToBeDeleted.contains(parent)) {
708             invalidIDs.add(wrapper.id);
709             continue nextEvent;
710           }
711           parent = parent.getParent();
712         }
713
714         if (candidate.isDirectory()) {
715           dirsToBeDeleted.add(candidate);
716         }
717       }
718     }
719
720     final List<VFileEvent> filtered = new ArrayList<>(events.size() - invalidIDs.size());
721     for (int i = 0, size = events.size(); i < size; i++) {
722       final VFileEvent event = events.get(i);
723       if (event.isValid() && !(event instanceof VFileDeleteEvent && invalidIDs.contains(i))) {
724         filtered.add(event);
725       }
726     }
727     return filtered;
728   }
729
730   @Override
731   public void processEvents(@NotNull List<VFileEvent> events) {
732     ApplicationManager.getApplication().assertWriteAccessAllowed();
733
734     List<VFileEvent> validated = validateEvents(events);
735
736     BulkFileListener publisher = myEventBus.syncPublisher(VirtualFileManager.VFS_CHANGES);
737     publisher.before(validated);
738
739     THashMap<VirtualFile, List<VFileEvent>> parentToChildrenEventsChanges = null;
740     for (VFileEvent event : validated) {
741       VirtualFile changedParent = null;
742       if (event instanceof VFileCreateEvent) {
743         changedParent = ((VFileCreateEvent)event).getParent();
744         ((VFileCreateEvent)event).resetCache();
745       }
746       else if (event instanceof VFileDeleteEvent) {
747         changedParent = ((VFileDeleteEvent)event).getFile().getParent();
748       }
749
750       if (changedParent != null) {
751         if (parentToChildrenEventsChanges == null) parentToChildrenEventsChanges = new THashMap<>();
752         List<VFileEvent> parentChildrenChanges = parentToChildrenEventsChanges.get(changedParent);
753         if (parentChildrenChanges == null) {
754           parentToChildrenEventsChanges.put(changedParent, parentChildrenChanges = new SmartList<>());
755         }
756         parentChildrenChanges.add(event);
757       }
758       else {
759         applyEvent(event);
760       }
761     }
762
763     if (parentToChildrenEventsChanges != null) {
764       parentToChildrenEventsChanges.forEachEntry(new TObjectObjectProcedure<VirtualFile, List<VFileEvent>>() {
765         @Override
766         public boolean execute(VirtualFile parent, List<VFileEvent> childrenEvents) {
767           applyChildrenChangeEvents(parent, childrenEvents);
768           return true;
769         }
770       });
771       parentToChildrenEventsChanges.clear();
772     }
773
774     publisher.after(validated);
775   }
776
777   private void applyChildrenChangeEvents(@NotNull VirtualFile parent, @NotNull List<VFileEvent> events) {
778     final NewVirtualFileSystem delegate = getDelegate(parent);
779     TIntArrayList childrenIdsUpdated = new TIntArrayList();
780     List<VirtualFile> childrenToBeUpdated = new SmartList<>();
781
782     final int parentId = getFileId(parent);
783     assert parentId != 0;
784     TIntHashSet parentChildrenIds = new TIntHashSet(FSRecords.list(parentId));
785     boolean hasRemovedChildren = false;
786
787     for (VFileEvent event : events) {
788       if (event instanceof VFileCreateEvent) {
789         String name = ((VFileCreateEvent)event).getChildName();
790         final VirtualFile fake = new FakeVirtualFile(parent, name);
791         final FileAttributes attributes = delegate.getAttributes(fake);
792
793         if (attributes != null) {
794           final int childId = createAndFillRecord(delegate, fake, parentId, attributes);
795           assert parent instanceof VirtualDirectoryImpl : parent;
796           final VirtualDirectoryImpl dir = (VirtualDirectoryImpl)parent;
797           VirtualFileSystemEntry child = dir.createChild(name, childId, dir.getFileSystem());
798           childrenToBeUpdated.add(child);
799           childrenIdsUpdated.add(childId);
800           parentChildrenIds.add(childId);
801         }
802       }
803       else if (event instanceof VFileDeleteEvent) {
804         VirtualFile file = ((VFileDeleteEvent)event).getFile();
805         if (!file.exists()) {
806           LOG.error("Deleting a file, which does not exist: " + file.getPath());
807           continue;
808         }
809
810         hasRemovedChildren = true;
811         int id = getFileId(file);
812
813         childrenToBeUpdated.add(file);
814         childrenIdsUpdated.add(-id);
815         parentChildrenIds.remove(id);
816       }
817     }
818
819     FSRecords.updateList(parentId, parentChildrenIds.toArray());
820
821     if (hasRemovedChildren) clearIdCache();
822     VirtualDirectoryImpl parentImpl = (VirtualDirectoryImpl)parent;
823
824     for (int i = 0, len = childrenIdsUpdated.size(); i < len; ++i) {
825       final int childId = childrenIdsUpdated.get(i);
826       final VirtualFile childFile = childrenToBeUpdated.get(i);
827
828       if (childId > 0) {
829         parentImpl.addChild((VirtualFileSystemEntry)childFile);
830       }
831       else {
832         FSRecords.deleteRecordRecursively(-childId);
833         parentImpl.removeChild(childFile);
834         invalidateSubtree(childFile);
835       }
836     }
837   }
838
839   @Override
840   @Nullable
841   public VirtualFileSystemEntry findRoot(@NotNull final String basePath, @NotNull NewVirtualFileSystem fs) {
842     if (basePath.isEmpty()) {
843       LOG.error("Invalid root, fs=" + fs);
844       return null;
845     }
846
847     String rootUrl = normalizeRootUrl(basePath, fs);
848
849     VirtualFileSystemEntry root = myRoots.get(rootUrl);
850     if (root != null) return root;
851
852     String rootName;
853     if (fs instanceof ArchiveFileSystem) {
854       VirtualFile localFile = ((ArchiveFileSystem)fs).findLocalByRootPath(basePath);
855       if (localFile == null) return null;
856       rootName = localFile.getName();
857     }
858     else {
859       rootName = basePath;
860     }
861
862     FileAttributes attributes = fs.getAttributes(new StubVirtualFile() {
863       @NotNull
864       @Override
865       public String getPath() {
866         return basePath;
867       }
868
869       @Nullable
870       @Override
871       public VirtualFile getParent() {
872         return null;
873       }
874     });
875     if (attributes == null || !attributes.isDirectory()) {
876       return null;
877     }
878
879     int rootId = FSRecords.findRootRecord(rootUrl);
880
881     VfsData.Segment segment = VfsData.getSegment(rootId, true);
882     VfsData.DirectoryData directoryData = new VfsData.DirectoryData();
883     VirtualFileSystemEntry newRoot = new FsRoot(rootId, segment, directoryData, fs, rootName, StringUtil.trimEnd(basePath, "/"));
884
885     boolean mark;
886     synchronized (myRoots) {
887       root = myRoots.get(rootUrl);
888       if (root != null) return root;
889
890       try {
891         VfsData.initFile(rootId, segment, -1, directoryData);
892       }
893       catch (VfsData.FileAlreadyCreatedException e) {
894         for (Map.Entry<String, VirtualFileSystemEntry> entry : myRoots.entrySet()) {
895           final VirtualFileSystemEntry existingRoot = entry.getValue();
896           if (Math.abs(existingRoot.getId()) == rootId) {
897             throw new RuntimeException("Duplicate FS roots: " + rootUrl + " and " + entry.getKey() + ", id=" + rootId + ", valid=" + existingRoot.isValid(), e);
898           }
899         }
900         throw new RuntimeException("No root duplication, roots=" + Arrays.toString(FSRecords.listAll(1)), e);
901       }
902       incStructuralModificationCount();
903       mark = writeAttributesToRecord(rootId, 0, newRoot, fs, attributes);
904
905       myRoots.put(rootUrl, newRoot);
906       myRootsById.put(rootId, newRoot);
907       myIdToDirCache.put(rootId, newRoot);
908     }
909
910     if (!mark && attributes.lastModified != FSRecords.getTimestamp(rootId)) {
911       newRoot.markDirtyRecursively();
912     }
913
914     LOG.assertTrue(rootId == newRoot.getId(), "root=" + newRoot + " expected=" + rootId + " actual=" + newRoot.getId());
915
916     return newRoot;
917   }
918
919   @NotNull
920   private static String normalizeRootUrl(@NotNull String basePath, @NotNull NewVirtualFileSystem fs) {
921     // need to protect against relative path of the form "/x/../y"
922     String normalized = VfsImplUtil.normalize(fs, FileUtil.toCanonicalPath(basePath));
923     String protocol = fs.getProtocol();
924     StringBuilder result = new StringBuilder(protocol.length() + URLUtil.SCHEME_SEPARATOR.length() + normalized.length());
925     result.append(protocol).append(URLUtil.SCHEME_SEPARATOR).append(normalized);
926     return StringUtil.endsWithChar(result, '/') ? UriUtil.trimTrailingSlashes(result.toString()) : result.toString();
927   }
928
929   @Override
930   public void clearIdCache() {
931     // remove all except myRootsById contents
932     for (Iterator<ConcurrentIntObjectMap.IntEntry<VirtualFileSystemEntry>> iterator = myIdToDirCache.entries().iterator(); iterator.hasNext(); ) {
933       ConcurrentIntObjectMap.IntEntry<VirtualFileSystemEntry> entry = iterator.next();
934       int id = entry.getKey();
935       if (!myRootsById.containsKey(id)) {
936         iterator.remove();
937       }
938     }
939   }
940
941   @Override
942   @Nullable
943   public NewVirtualFile findFileById(final int id) {
944     return findFileById(id, false);
945   }
946
947   @Override
948   public NewVirtualFile findFileByIdIfCached(final int id) {
949     return findFileById(id, true);
950   }
951
952   @Nullable
953   private VirtualFileSystemEntry findFileById(int id, boolean cachedOnly) {
954     VirtualFileSystemEntry cached = myIdToDirCache.get(id);
955     if (cached != null) return cached;
956
957     TIntArrayList parents = FSRecords.getParents(id, myIdToDirCache);
958     // the last element of the parents is either a root or already cached element
959     int parentId = parents.get(parents.size() - 1);
960     VirtualFileSystemEntry result = myIdToDirCache.get(parentId);
961
962     for (int i=parents.size() - 2; i>=0; i--) {
963       if (!(result instanceof VirtualDirectoryImpl)) {
964         return null;
965       }
966       parentId = parents.get(i);
967       result = ((VirtualDirectoryImpl)result).findChildById(parentId, cachedOnly);
968       if (result instanceof VirtualDirectoryImpl) {
969         VirtualFileSystemEntry old = myIdToDirCache.putIfAbsent(parentId, result);
970         if (old != null) result = old;
971       }
972     }
973
974     return result;
975   }
976
977   @Override
978   @NotNull
979   public VirtualFile[] getRoots() {
980     Collection<VirtualFileSystemEntry> roots = myRoots.values();
981     return ArrayUtil.stripTrailingNulls(VfsUtilCore.toVirtualFileArray(roots));
982   }
983
984   @Override
985   @NotNull
986   public VirtualFile[] getRoots(@NotNull final NewVirtualFileSystem fs) {
987     final List<VirtualFile> roots = new ArrayList<>();
988
989     for (NewVirtualFile root : myRoots.values()) {
990       if (root.getFileSystem() == fs) {
991         roots.add(root);
992       }
993     }
994
995     return VfsUtilCore.toVirtualFileArray(roots);
996   }
997
998   @Override
999   @NotNull
1000   public VirtualFile[] getLocalRoots() {
1001     List<VirtualFile> roots = ContainerUtil.newSmartList();
1002
1003     for (NewVirtualFile root : myRoots.values()) {
1004       if (root.isInLocalFileSystem() && !(root.getFileSystem() instanceof TempFileSystem)) {
1005         roots.add(root);
1006       }
1007     }
1008     return VfsUtilCore.toVirtualFileArray(roots);
1009   }
1010
1011   private VirtualFileSystemEntry applyEvent(@NotNull VFileEvent event) {
1012     if (LOG.isDebugEnabled()) {
1013       LOG.debug("Applying " + event);
1014     }
1015     try {
1016       if (event instanceof VFileCreateEvent) {
1017         final VFileCreateEvent createEvent = (VFileCreateEvent)event;
1018         return executeCreateChild(createEvent.getParent(), createEvent.getChildName());
1019       }
1020       else if (event instanceof VFileDeleteEvent) {
1021         final VFileDeleteEvent deleteEvent = (VFileDeleteEvent)event;
1022         executeDelete(deleteEvent.getFile());
1023       }
1024       else if (event instanceof VFileContentChangeEvent) {
1025         final VFileContentChangeEvent contentUpdateEvent = (VFileContentChangeEvent)event;
1026         executeTouch(contentUpdateEvent.getFile(), contentUpdateEvent.isFromRefresh(), contentUpdateEvent.getModificationStamp());
1027       }
1028       else if (event instanceof VFileCopyEvent) {
1029         final VFileCopyEvent copyEvent = (VFileCopyEvent)event;
1030         return executeCreateChild(copyEvent.getNewParent(), copyEvent.getNewChildName());
1031       }
1032       else if (event instanceof VFileMoveEvent) {
1033         final VFileMoveEvent moveEvent = (VFileMoveEvent)event;
1034         executeMove(moveEvent.getFile(), moveEvent.getNewParent());
1035       }
1036       else if (event instanceof VFilePropertyChangeEvent) {
1037         final VFilePropertyChangeEvent propertyChangeEvent = (VFilePropertyChangeEvent)event;
1038         VirtualFile file = propertyChangeEvent.getFile();
1039         Object newValue = propertyChangeEvent.getNewValue();
1040         if (VirtualFile.PROP_NAME.equals(propertyChangeEvent.getPropertyName())) {
1041           executeRename(file, (String)newValue);
1042         }
1043         else if (VirtualFile.PROP_WRITABLE.equals(propertyChangeEvent.getPropertyName())) {
1044           executeSetWritable(file, ((Boolean)newValue).booleanValue());
1045           if (LOG.isDebugEnabled()) {
1046             LOG.debug("File " + file + " writable=" + file.isWritable() + " id=" + getFileId(file));
1047           }
1048         }
1049         else if (VirtualFile.PROP_HIDDEN.equals(propertyChangeEvent.getPropertyName())) {
1050           executeSetHidden(file, ((Boolean)newValue).booleanValue());
1051         }
1052         else if (VirtualFile.PROP_SYMLINK_TARGET.equals(propertyChangeEvent.getPropertyName())) {
1053           executeSetTarget(file, (String)newValue);
1054         }
1055       }
1056     }
1057     catch (Exception e) {
1058       // Exception applying single event should not prevent other events from applying.
1059       LOG.error(e);
1060     }
1061     return null;
1062   }
1063
1064   @NotNull
1065   @NonNls
1066   public String toString() {
1067     return "PersistentFS";
1068   }
1069
1070   private static VirtualFileSystemEntry executeCreateChild(@NotNull VirtualFile parent, @NotNull String name) {
1071     final NewVirtualFileSystem delegate = getDelegate(parent);
1072     final VirtualFile fake = new FakeVirtualFile(parent, name);
1073     final FileAttributes attributes = delegate.getAttributes(fake);
1074     if (attributes != null) {
1075       final int parentId = getFileId(parent);
1076       final int childId = createAndFillRecord(delegate, fake, parentId, attributes);
1077       appendIdToParentList(parentId, childId);
1078       assert parent instanceof VirtualDirectoryImpl : parent;
1079       final VirtualDirectoryImpl dir = (VirtualDirectoryImpl)parent;
1080       VirtualFileSystemEntry child = dir.createChild(name, childId, dir.getFileSystem());
1081       dir.addChild(child);
1082       return child;
1083     }
1084     return null;
1085   }
1086
1087   private static int createAndFillRecord(@NotNull NewVirtualFileSystem delegateSystem,
1088                                          @NotNull VirtualFile delegateFile,
1089                                          int parentId,
1090                                          @NotNull FileAttributes attributes) {
1091     final int childId = FSRecords.createRecord();
1092     writeAttributesToRecord(childId, parentId, delegateFile, delegateSystem, attributes);
1093     return childId;
1094   }
1095
1096   private static void appendIdToParentList(final int parentId, final int childId) {
1097     int[] childrenList = FSRecords.list(parentId);
1098     childrenList = ArrayUtil.append(childrenList, childId);
1099     FSRecords.updateList(parentId, childrenList);
1100   }
1101
1102   private void executeDelete(@NotNull VirtualFile file) {
1103     if (!file.exists()) {
1104       LOG.error("Deleting a file, which does not exist: " + file.getPath());
1105       return;
1106     }
1107     clearIdCache();
1108
1109     int id = getFileId(file);
1110
1111     final VirtualFile parent = file.getParent();
1112     final int parentId = parent == null ? 0 : getFileId(parent);
1113
1114     if (parentId == 0) {
1115       String rootUrl = normalizeRootUrl(file.getPath(), (NewVirtualFileSystem)file.getFileSystem());
1116       synchronized (myRoots) {
1117         myRoots.remove(rootUrl);
1118         myRootsById.remove(id);
1119         myIdToDirCache.remove(id);
1120         FSRecords.deleteRootRecord(id);
1121       }
1122     }
1123     else {
1124       removeIdFromParentList(parentId, id, parent, file);
1125       VirtualDirectoryImpl directory = (VirtualDirectoryImpl)file.getParent();
1126       assert directory != null : file;
1127       directory.removeChild(file);
1128     }
1129
1130     FSRecords.deleteRecordRecursively(id);
1131
1132     invalidateSubtree(file);
1133     incStructuralModificationCount();
1134   }
1135
1136   private static void invalidateSubtree(@NotNull VirtualFile file) {
1137     final VirtualFileSystemEntry impl = (VirtualFileSystemEntry)file;
1138     impl.invalidate();
1139     for (VirtualFile child : impl.getCachedChildren()) {
1140       invalidateSubtree(child);
1141     }
1142   }
1143
1144   private static void removeIdFromParentList(final int parentId, final int id, @NotNull VirtualFile parent, VirtualFile file) {
1145     int[] childList = FSRecords.list(parentId);
1146
1147     int index = ArrayUtil.indexOf(childList, id);
1148     if (index == -1) {
1149       throw new RuntimeException("Cannot find child (" + id + ")" + file
1150                                  + "\n\tin (" + parentId + ")" + parent
1151                                  + "\n\tactual children:" + Arrays.toString(childList));
1152     }
1153     childList = ArrayUtil.remove(childList, index);
1154     FSRecords.updateList(parentId, childList);
1155   }
1156
1157   private static void executeRename(@NotNull VirtualFile file, @NotNull final String newName) {
1158     final int id = getFileId(file);
1159     FSRecords.setName(id, newName);
1160     ((VirtualFileSystemEntry)file).setNewName(newName);
1161   }
1162
1163   private static void executeSetWritable(@NotNull VirtualFile file, boolean writableFlag) {
1164     setFlag(file, IS_READ_ONLY, !writableFlag);
1165     ((VirtualFileSystemEntry)file).updateProperty(VirtualFile.PROP_WRITABLE, writableFlag);
1166   }
1167
1168   private static void executeSetHidden(@NotNull VirtualFile file, boolean hiddenFlag) {
1169     setFlag(file, IS_HIDDEN, hiddenFlag);
1170     ((VirtualFileSystemEntry)file).updateProperty(VirtualFile.PROP_HIDDEN, hiddenFlag);
1171   }
1172
1173   private static void executeSetTarget(@NotNull VirtualFile file, String target) {
1174     ((VirtualFileSystemEntry)file).setLinkTarget(target);
1175   }
1176
1177   private static void setFlag(@NotNull VirtualFile file, int mask, boolean value) {
1178     setFlag(getFileId(file), mask, value);
1179   }
1180
1181   private static void setFlag(final int id, final int mask, final boolean value) {
1182     int oldFlags = FSRecords.getFlags(id);
1183     int flags = value ? oldFlags | mask : oldFlags & ~mask;
1184
1185     if (oldFlags != flags) {
1186       FSRecords.setFlags(id, flags, true);
1187     }
1188   }
1189
1190   private static boolean checkFlag(int fileId, int mask) {
1191     return BitUtil.isSet(FSRecords.getFlags(fileId), mask);
1192   }
1193
1194   private static void executeTouch(@NotNull VirtualFile file, boolean reloadContentFromDelegate, long newModificationStamp) {
1195     if (reloadContentFromDelegate) {
1196       setFlag(file, MUST_RELOAD_CONTENT, true);
1197     }
1198
1199     final NewVirtualFileSystem delegate = getDelegate(file);
1200     final FileAttributes attributes = delegate.getAttributes(file);
1201     FSRecords.setLength(getFileId(file), attributes != null ? attributes.length : DEFAULT_LENGTH);
1202     FSRecords.setTimestamp(getFileId(file), attributes != null ? attributes.lastModified : DEFAULT_TIMESTAMP);
1203
1204     ((VirtualFileSystemEntry)file).setModificationStamp(newModificationStamp);
1205   }
1206
1207   private void executeMove(@NotNull VirtualFile file, @NotNull VirtualFile newParent) {
1208     clearIdCache();
1209
1210     final int fileId = getFileId(file);
1211     final int newParentId = getFileId(newParent);
1212     final int oldParentId = getFileId(file.getParent());
1213
1214     removeIdFromParentList(oldParentId, fileId, file.getParent(), file);
1215     FSRecords.setParent(fileId, newParentId);
1216     appendIdToParentList(newParentId, fileId);
1217     ((VirtualFileSystemEntry)file).setParent(newParent);
1218   }
1219
1220   @Override
1221   public String getName(int id) {
1222     assert id > 0;
1223     return FSRecords.getName(id);
1224   }
1225
1226   @TestOnly
1227   public void cleanPersistedContents() {
1228     final int[] roots = FSRecords.listRoots();
1229     for (int root : roots) {
1230       cleanPersistedContentsRecursively(root);
1231     }
1232   }
1233
1234   @TestOnly
1235   private void cleanPersistedContentsRecursively(int id) {
1236     if (isDirectory(getFileAttributes(id))) {
1237       for (int child : FSRecords.list(id)) {
1238         cleanPersistedContentsRecursively(child);
1239       }
1240     }
1241     else {
1242       setFlag(id, MUST_RELOAD_CONTENT, true);
1243     }
1244   }
1245
1246
1247   private static class FsRoot extends VirtualDirectoryImpl {
1248     private final String myName;
1249     private final String myPathBeforeSlash;
1250
1251     private FsRoot(int id, VfsData.Segment segment, VfsData.DirectoryData data, NewVirtualFileSystem fs, String name, String pathBeforeSlash) {
1252       super(id, segment, data, null, fs);
1253       myName = name;
1254       myPathBeforeSlash = pathBeforeSlash;
1255     }
1256
1257     @NotNull
1258     @Override
1259     public CharSequence getNameSequence() {
1260       return myName;
1261     }
1262
1263     @Override
1264     protected char[] appendPathOnFileSystem(int pathLength, int[] position) {
1265       char[] chars = new char[pathLength + myPathBeforeSlash.length()];
1266       position[0] = copyString(chars, position[0], myPathBeforeSlash);
1267       return chars;
1268     }
1269
1270     @Override
1271     public void setNewName(@NotNull String newName) {
1272       throw new IncorrectOperationException();
1273     }
1274
1275     @Override
1276     public final void setParent(@NotNull VirtualFile newParent) {
1277       throw new IncorrectOperationException();
1278     }
1279
1280     @NotNull
1281     @Override
1282     public String getPath() {
1283       return myPathBeforeSlash + '/';
1284     }
1285
1286     @NotNull
1287     @Override
1288     public String getUrl() {
1289       return getFileSystem().getProtocol() + "://" + getPath();
1290     }
1291   }
1292 }