02325b2d31416b56a98a193571082befe2ef8adf
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / vfs / newvfs / impl / VirtualDirectoryImpl.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.impl;
17
18 import com.intellij.openapi.application.Application;
19 import com.intellij.openapi.application.ApplicationManager;
20 import com.intellij.openapi.application.PathManager;
21 import com.intellij.openapi.application.impl.ApplicationImpl;
22 import com.intellij.openapi.module.Module;
23 import com.intellij.openapi.module.ModuleManager;
24 import com.intellij.openapi.project.Project;
25 import com.intellij.openapi.project.ProjectManager;
26 import com.intellij.openapi.roots.ModuleRootManager;
27 import com.intellij.openapi.roots.OrderEntry;
28 import com.intellij.openapi.roots.OrderRootType;
29 import com.intellij.openapi.roots.ProjectRootManager;
30 import com.intellij.openapi.util.Condition;
31 import com.intellij.openapi.util.Pair;
32 import com.intellij.openapi.util.io.FileUtil;
33 import com.intellij.openapi.util.text.StringUtil;
34 import com.intellij.openapi.vfs.JarFileSystem;
35 import com.intellij.openapi.vfs.LocalFileSystem;
36 import com.intellij.openapi.vfs.VfsUtil;
37 import com.intellij.openapi.vfs.VirtualFile;
38 import com.intellij.openapi.vfs.newvfs.NewVirtualFile;
39 import com.intellij.openapi.vfs.newvfs.NewVirtualFileSystem;
40 import com.intellij.openapi.vfs.newvfs.RefreshQueue;
41 import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent;
42 import com.intellij.openapi.vfs.newvfs.persistent.PersistentFS;
43 import com.intellij.util.ArrayUtil;
44 import com.intellij.util.PathUtil;
45 import com.intellij.util.SystemProperties;
46 import com.intellij.util.containers.ContainerUtil;
47 import com.intellij.util.text.CaseInsensitiveStringHashingStrategy;
48 import gnu.trove.THashMap;
49 import gnu.trove.THashSet;
50 import org.jetbrains.annotations.NotNull;
51 import org.jetbrains.annotations.Nullable;
52 import org.jetbrains.annotations.TestOnly;
53
54 import java.io.File;
55 import java.io.IOException;
56 import java.io.InputStream;
57 import java.io.OutputStream;
58 import java.net.URISyntaxException;
59 import java.net.URL;
60 import java.util.*;
61
62 /**
63  * @author max
64  */
65 public class VirtualDirectoryImpl extends VirtualFileSystemEntry {
66   private static final VirtualFileSystemEntry NULL_VIRTUAL_FILE = new VirtualFileImpl("*?;%NULL", null, -42);
67
68   private final NewVirtualFileSystem myFS;
69
70   // guarded by this
71   protected Object myChildren; // Either HashMap<String, VFile> or VFile[]
72
73   public VirtualDirectoryImpl(@NotNull String name, final VirtualDirectoryImpl parent, @NotNull NewVirtualFileSystem fs, final int id) {
74     super(name, parent, id);
75     myFS = fs;
76   }
77
78   @Override
79   @NotNull
80   public NewVirtualFileSystem getFileSystem() {
81     return myFS;
82   }
83
84   @Nullable
85   private VirtualFileSystemEntry findChild(@NotNull String name,
86                                            final boolean createIfNotFound,
87                                            boolean ensureCanonicalName,
88                                            NewVirtualFileSystem delegate) {
89     final VirtualFileSystemEntry result = doFindChild(name, createIfNotFound, ensureCanonicalName, delegate);
90     if (result == NULL_VIRTUAL_FILE) {
91       return createIfNotFound ? createAndFindChildWithEventFire(name) : null;
92     }
93
94     if (result == null) {
95       synchronized (this) {
96         Map<String, VirtualFileSystemEntry> map = asMap();
97         if (map != null) {
98           map.put(name, NULL_VIRTUAL_FILE);
99         }
100       }
101     }
102
103     return result;
104   }
105
106   @Nullable
107   private VirtualFileSystemEntry doFindChild(@NotNull String name,
108                                              final boolean createIfNotFound,
109                                              boolean ensureCanonicalName,
110                                              NewVirtualFileSystem delegate) {
111     if (name.length() == 0) {
112       return null;
113     }
114
115     final VirtualFileSystemEntry[] array;
116     final Map<String, VirtualFileSystemEntry> map;
117     final VirtualFileSystemEntry file;
118     synchronized (this) {
119       array = asArray();
120       if (array == null) {
121         map = ensureAsMap();
122         file = map.get(name);
123       }
124       else {
125         file = null;
126         map = null;
127       }
128     }
129     if (array != null) {
130       final boolean ignoreCase = !getFileSystem().isCaseSensitive();
131       for (VirtualFileSystemEntry vf : array) {
132         if (vf.nameMatches(name, ignoreCase)) return vf;
133       }
134       return createIfNotFound ? createAndFindChildWithEventFire(name) : null;
135     }
136
137     if (file != null) return file;
138
139     if (ensureCanonicalName) {
140       VirtualFile fake = new FakeVirtualFile(this, name);
141       name = delegate.getCanonicallyCasedName(fake);
142       if (name.length() == 0) return null;
143     }
144
145     synchronized (this) {
146       // do not extract getId outside the synchronized block since it will cause a concurrency problem.
147       int id = PersistentFS.getId(this, name, delegate);
148       if (id > 0) {
149         // maybe another doFindChild() sneaked in the middle
150         VirtualFileSystemEntry lastTry = map.get(name);
151         if (lastTry != null) return lastTry;
152
153         final String shorty = new String(name);
154         VirtualFileSystemEntry child = createChild(shorty, id); // So we don't hold whole char[] buffer of a lengthy path
155         map.put(shorty, child);
156         return child;
157       }
158     }
159
160     return null;
161   }
162
163   @NotNull
164   public VirtualFileSystemEntry createChild(String name, int id) {
165     final VirtualFileSystemEntry child;
166     final NewVirtualFileSystem fs = getFileSystem();
167     if (PersistentFS.isDirectory(id)) {
168       child = /*PersistentFS.isSymLink(id) ? new SymlinkDirectory(name, this, fs, id) :*/ new VirtualDirectoryImpl(name, this, fs, id);
169     }
170     else {
171       child = new VirtualFileImpl(name, this, id);
172       assertAccessInTests(child);
173     }
174
175     if (fs.markNewFilesAsDirty()) {
176       child.markDirty();
177     }
178
179     return child;
180   }
181
182
183   private static final boolean IS_UNDER_TEAMCITY = System.getProperty("bootstrap.testcases") != null;
184   private static final boolean IS_UNIT_TESTS = ApplicationManager.getApplication().isUnitTestMode();
185   private static final Collection<String> additionalRoots = new THashSet<String>();
186   @TestOnly
187   public static void allowToAccess(@NotNull String root) { additionalRoots.add(FileUtil.toSystemIndependentName(root)); }
188   @TestOnly
189   private static void assertAccessInTests(VirtualFileSystemEntry child) {
190     if (IS_UNIT_TESTS && IS_UNDER_TEAMCITY && ApplicationManager.getApplication() instanceof ApplicationImpl && ((ApplicationImpl)ApplicationManager.getApplication()).isComponentsCreated()) {
191       NewVirtualFileSystem fileSystem = child.getFileSystem();
192       if (fileSystem != LocalFileSystem.getInstance() && fileSystem != JarFileSystem.getInstance()) {
193         return;
194       }
195       // root' children are loaded always
196       if (child.getParent() == null || child.getParent().getParent() == null) return;
197
198       Set<String> allowed = allowedRoots();
199       boolean isUnder = allowed == null;
200       if (!isUnder) {
201         for (String root : allowed) {
202           String childPath = child.getPath();
203           if (child.getFileSystem() == JarFileSystem.getInstance()) {
204             VirtualFile local = JarFileSystem.getInstance().getVirtualFileForJar(child);
205             childPath = local.getPath();
206           }
207           if (FileUtil.startsWith(childPath, root)) {
208             isUnder = true;
209             break;
210           }
211           if (root.startsWith(JarFileSystem.PROTOCOL_PREFIX)) {
212             String rootLocalPath = FileUtil.toSystemIndependentName(PathUtil.toPresentableUrl(root));
213             isUnder = FileUtil.startsWith(childPath, rootLocalPath);
214             if (isUnder) break;
215           }
216         }
217       }
218
219       if (!isUnder) {
220         if (!allowed.isEmpty()) {
221           assert false : "File accessed outside allowed roots: " + child +";\n Allowed roots: "+new ArrayList(allowed);
222         }
223       }
224     }
225   }
226
227   // null means we were unable to get roots, so do not check access
228   private static Set<String> allowedRoots() {
229     if (insideGettingRoots) return null;
230     Project[] openProjects = ProjectManager.getInstance().getOpenProjects();
231     if (openProjects.length == 0) return null;
232     final Set<String> allowed = new THashSet<String>();
233     String homePath = PathManager.getHomePath();
234     allowed.add(FileUtil.toSystemIndependentName(homePath));
235     try {
236       URL outUrl = Application.class.getResource("/");
237       String output = new File(outUrl.toURI()).getParentFile().getParentFile().getPath();
238       allowed.add(FileUtil.toSystemIndependentName(output));
239     }
240     catch (URISyntaxException ignored) {
241     }
242     String javaHome = SystemProperties.getJavaHome();
243     allowed.add(FileUtil.toSystemIndependentName(javaHome));
244     String tempDirectorySpecific = new File(FileUtil.getTempDirectory()).getParent();
245     allowed.add(FileUtil.toSystemIndependentName(tempDirectorySpecific));
246     String tempDirectory = System.getProperty("java.io.tmpdir");
247     allowed.add(FileUtil.toSystemIndependentName(tempDirectory));
248     String userHome = SystemProperties.getUserHome();
249     allowed.add(FileUtil.toSystemIndependentName(userHome));
250     for (Project project : openProjects) {
251       if (!project.isInitialized()) {
252         return null; // all is allowed
253       }
254       for (VirtualFile root : ProjectRootManager.getInstance(project).getContentRoots()) {
255         allowed.add(root.getPath());
256       }
257       for (VirtualFile root : getAllRoots(project)) {
258         allowed.add(StringUtil.trimEnd(root.getPath(), JarFileSystem.JAR_SEPARATOR));
259       }
260       String location = project.getLocation();
261       allowed.add(FileUtil.toSystemIndependentName(location));
262     }
263
264     //for (Sdk sdk : ProjectJdkTable.getInstance().getAllJdks()) {
265     //  allowed.add(FileUtil.toSystemIndependentName(sdk.getHomePath()));
266     //}
267     for (String root : additionalRoots) {
268       allowed.add(root);
269     }
270     return allowed;
271   }
272
273   private static boolean insideGettingRoots;
274   private static VirtualFile[] getAllRoots(Project project) {
275     insideGettingRoots = true;
276     Set<VirtualFile> roots = new THashSet<VirtualFile>();
277     final Module[] modules = ModuleManager.getInstance(project).getModules();
278     for (Module module : modules) {
279       final ModuleRootManager moduleRootManager = ModuleRootManager.getInstance(module);
280       final OrderEntry[] orderEntries = moduleRootManager.getOrderEntries();
281       for (OrderEntry entry : orderEntries) {
282         VirtualFile[] files;
283         files = entry.getFiles(OrderRootType.CLASSES);
284         ContainerUtil.addAll(roots, files);
285         files = entry.getFiles(OrderRootType.SOURCES);
286         ContainerUtil.addAll(roots, files);
287         files = entry.getFiles(OrderRootType.CLASSES_AND_OUTPUT);
288         ContainerUtil.addAll(roots, files);
289       }
290     }
291     insideGettingRoots = false;
292     return VfsUtil.toVirtualFileArray(roots);
293   }
294
295
296   @Nullable
297   private VirtualFileSystemEntry createAndFindChildWithEventFire(@NotNull String name) {
298     final NewVirtualFileSystem delegate = getFileSystem();
299     VirtualFile fake = new FakeVirtualFile(this, name);
300     if (delegate.exists(fake)) {
301       final String realName = delegate.getCanonicallyCasedName(fake);
302       VFileCreateEvent event = new VFileCreateEvent(null, this, realName, delegate.isDirectory(fake), true);
303       RefreshQueue.getInstance().processSingleEvent(event);
304       return findChild(realName);
305     }
306     else {
307       return null;
308     }
309   }
310
311   @Override
312   @Nullable
313   public NewVirtualFile refreshAndFindChild(@NotNull String name) {
314     return findChild(name, true, true, getFileSystem());
315   }
316
317   @Override
318   @Nullable
319   public synchronized NewVirtualFile findChildIfCached(@NotNull String name) {
320     final VirtualFileSystemEntry[] a = asArray();
321     if (a != null) {
322       final boolean ignoreCase = !getFileSystem().isCaseSensitive();
323       for (VirtualFileSystemEntry file : a) {
324         if (file.nameMatches(name, ignoreCase)) return file;
325       }
326
327       return null;
328     }
329
330     final Map<String, VirtualFileSystemEntry> map = asMap();
331     if (map != null) {
332       final VirtualFileSystemEntry file = map.get(name);
333       return file != NULL_VIRTUAL_FILE ? file : null;
334     }
335
336     return null;
337   }
338
339   @Override
340   @NotNull
341   public Iterable<VirtualFile> iterInDbChildren() {
342     return ContainerUtil.iterate(getInDbChildren(), new Condition<VirtualFile>() {
343       @Override
344       public boolean value(VirtualFile file) {
345         return file != NULL_VIRTUAL_FILE;
346       }
347     });
348   }
349
350   @NotNull
351   private synchronized Collection<VirtualFile> getInDbChildren() {
352     if (myChildren instanceof VirtualFileSystemEntry[]) {
353       return Arrays.asList((VirtualFile[])myChildren);
354     }
355
356     if (!ourPersistence.wereChildrenAccessed(this)) {
357       return Collections.emptyList();
358     }
359
360     if (ourPersistence.areChildrenLoaded(this)) {
361       return Arrays.asList(getChildren());
362     }
363
364     final String[] names = PersistentFS.listPersisted(this);
365     NewVirtualFileSystem delegate = PersistentFS.replaceWithNativeFS(getFileSystem());
366     for (String name : names) {
367       findChild(name, false, false, delegate);
368     }
369     
370     // important: should return a copy here for safe iterations
371     return new ArrayList<VirtualFile>(ensureAsMap().values());
372   }
373
374   @Override
375   @NotNull
376   public synchronized VirtualFile[] getChildren() {
377     if (myChildren instanceof VirtualFileSystemEntry[]) {
378       return (VirtualFileSystemEntry[])myChildren;
379     }
380
381     Pair<String[],int[]> pair = PersistentFS.listAll(this);
382     final int[] childrenIds = pair.second;
383     VirtualFileSystemEntry[] children;
384     if (childrenIds.length == 0) {
385       children = EMPTY_ARRAY;
386     }
387     else {
388       children = new VirtualFileSystemEntry[childrenIds.length];
389       String[] names = pair.first;
390       final Map<String, VirtualFileSystemEntry> map = asMap();
391       for (int i = 0; i < children.length; i++) {
392         final int childId = childrenIds[i];
393         final String name = names[i];
394         VirtualFileSystemEntry child = map != null ? map.get(name) : null;
395
396         children[i] = child != null && child != NULL_VIRTUAL_FILE ? child : createChild(name, childId);
397       }
398     }
399
400     if (getId() > 0) {
401       myChildren = children;
402     }
403
404     return children;
405   }
406
407   @Override
408   @Nullable
409   public VirtualFileSystemEntry findChild(@NotNull final String name) {
410     return findChild(name, false, true, getFileSystem());
411   }
412
413   @Override
414   @Nullable
415   public NewVirtualFile findChildById(int id) {
416     final NewVirtualFile loaded = findChildByIdIfCached(id);
417     if (loaded != null) {
418       return loaded;
419     }
420     
421     String name = ourPersistence.getName(id);
422     return findChild(name, false, false, getFileSystem());
423   }
424
425   @Override
426   public NewVirtualFile findChildByIdIfCached(int id) {
427     final VirtualFile[] a = asArray();
428     if (a != null) {
429       for (VirtualFile file : a) {
430         NewVirtualFile withId = (NewVirtualFile)file;
431         if (withId.getId() == id) return withId;
432       }
433
434       return null;
435     }
436     synchronized (this) {
437       final Map<String, VirtualFileSystemEntry> map = asMap();
438       if (map != null) {
439         for (Map.Entry<String, VirtualFileSystemEntry> entry : map.entrySet()) {
440           VirtualFile file = entry.getValue();
441           if (file == NULL_VIRTUAL_FILE) continue;
442           NewVirtualFile withId = (NewVirtualFile)file;
443           if (withId.getId() == id) return withId;
444         }
445       }
446     }
447     return null;
448   }
449
450   @Nullable
451   private VirtualFileSystemEntry[] asArray() {
452     if (myChildren instanceof VirtualFileSystemEntry[]) return (VirtualFileSystemEntry[])myChildren;
453     return null;
454   }
455
456   @Nullable
457   private Map<String, VirtualFileSystemEntry> asMap() {
458     if (myChildren instanceof Map) {
459       //noinspection unchecked
460       return (Map<String, VirtualFileSystemEntry>)myChildren;
461     }
462     return null;
463   }
464
465   @NotNull
466   private Map<String, VirtualFileSystemEntry> ensureAsMap() {
467     Map<String, VirtualFileSystemEntry> map;
468     if (myChildren == null) {
469       map = createMap();
470       myChildren = map;
471     }
472     else {
473       //noinspection unchecked
474       map = (Map<String, VirtualFileSystemEntry>)myChildren;
475     }
476
477     return map;
478   }
479
480   public synchronized void addChild(@NotNull VirtualFileSystemEntry file) {
481     final VirtualFileSystemEntry[] a = asArray();
482     if (a != null) {
483       myChildren = ArrayUtil.append(a, file);
484     }
485     else {
486       ensureAsMap().put(file.getName(), file);
487     }
488   }
489
490   public synchronized void removeChild(@NotNull VirtualFile file) {
491     final VirtualFileSystemEntry[] a = asArray();
492     if (a != null) {
493       myChildren = ArrayUtil.remove(a, file);
494     }
495     else {
496       ensureAsMap().put(file.getName(), NULL_VIRTUAL_FILE);
497     }
498   }
499
500   public synchronized boolean allChildrenLoaded() {
501     return myChildren instanceof VirtualFileSystemEntry[];
502   }
503
504   @NotNull
505   public synchronized List<String> getSuspiciousNames() {
506     final Map<String, VirtualFileSystemEntry> map = asMap();
507     if (map == null) return Collections.emptyList();
508
509     List<String> names = new ArrayList<String>();
510     for (Map.Entry<String, VirtualFileSystemEntry> entry : map.entrySet()) {
511       if (entry.getValue() == NULL_VIRTUAL_FILE) {
512         names.add(entry.getKey());
513       }
514     }
515
516     return names;
517   }
518
519   @Override
520   public boolean isDirectory() {
521     return true;
522   }
523
524   @Override
525   @NotNull
526   public synchronized Collection<VirtualFile> getCachedChildren() {
527     final Map<String, VirtualFileSystemEntry> map = asMap();
528     if (map != null) {
529       Set<VirtualFile> files = new THashSet<VirtualFile>(map.values());
530       files.remove(NULL_VIRTUAL_FILE);
531       return files;
532     }
533
534     final VirtualFile[] a = asArray();
535     if (a != null) return Arrays.asList(a);
536
537     return Collections.emptyList();
538   }
539
540   @NotNull
541   private Map<String, VirtualFileSystemEntry> createMap() {
542     return getFileSystem().isCaseSensitive()
543            ? new THashMap<String, VirtualFileSystemEntry>()
544            : new THashMap<String, VirtualFileSystemEntry>(CaseInsensitiveStringHashingStrategy.INSTANCE);
545   }
546
547   @TestOnly
548   public synchronized void cleanupCachedChildren(@NotNull Set<VirtualFile> survivors) {
549     if (survivors.contains(this)) {
550       for (VirtualFile file : getCachedChildren()) {
551         if (file instanceof VirtualDirectoryImpl) {
552           ((VirtualDirectoryImpl)file).cleanupCachedChildren(survivors);
553         }
554       }
555     }
556     else {
557       myChildren = null;
558     }
559   }
560
561   @Override
562   public InputStream getInputStream() throws IOException {
563     throw new IOException("getInputStream() must not be called against a directory: " + getUrl());
564   }
565
566   @Override
567   @NotNull
568   public OutputStream getOutputStream(final Object requestor, final long newModificationStamp, final long newTimeStamp) throws IOException {
569     throw new IOException("getOutputStream() must not be called against a directory: " + getUrl());
570   }
571 }