Skip to main content
aboutsummaryrefslogtreecommitdiffstats
blob: 27999c85abad7edf6c1a8dd294c7c4a415628b9c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
/*******************************************************************************
 * Copyright (c) 2000, 2015 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Martin Oberhuber (Wind River) - [105554] handle cyclic symbolic links
 *     Martin Oberhuber (Wind River) - [232426] shared prefix histories for symlinks
 *     Serge Beauchamp (Freescale Semiconductor) - [252996] add resource filtering
 *     Martin Oberhuber (Wind River) - [292267] OutOfMemoryError due to leak in UnifiedTree
 *     James Blackburn (Broadcom Corp.) - ongoing development
 *     Lars Vogel <Lars.Vogel@vogella.com> - Bug 473427
 *     Sergey Prigogin (Google) -  ongoing development
 *******************************************************************************/
package org.eclipse.core.internal.localstore;

import java.io.IOException;
import java.util.*;
import java.util.regex.Pattern;
import org.eclipse.core.filesystem.*;
import org.eclipse.core.internal.refresh.RefreshJob;
import org.eclipse.core.internal.resources.*;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.jobs.Job;

/**
 * Represents the workspace's tree merged with the file system's tree.
 */
public class UnifiedTree {

	/** Skip advanced link checking, see bug 537449 */
	private static boolean disable_advanced_recursive_link_checks = System.getProperty("org.eclipse.core.resources.disable_advanced_recursive_link_checks") != null; //$NON-NLS-1$

	/** special node to mark the separation of a node's children */
	protected static final UnifiedTreeNode childrenMarker = new UnifiedTreeNode(null, null, null, null, false);

	private static final Iterator<UnifiedTreeNode> EMPTY_ITERATOR = Collections.EMPTY_LIST.iterator();

	/** special node to mark the beginning of a level in the tree */
	protected static final UnifiedTreeNode levelMarker = new UnifiedTreeNode(null, null, null, null, false);

	private static final IFileInfo[] NO_CHILDREN = {};

	/** Singleton to indicate no local children */
	private static final IResource[] NO_RESOURCES = {};

	/**
	 * True if the level of the children of the current node are valid according
	 * to the requested refresh depth, false otherwise
	 */
	protected boolean childLevelValid;

	/** an IFileTree which can be used to build a unified tree*/
	protected IFileTree fileTree;

	/** Spare node objects available for reuse */
	protected ArrayList<UnifiedTreeNode> freeNodes = new ArrayList<>();
	/** tree's actual level */
	protected int level;
	/** our queue */
	protected LinkedList<UnifiedTreeNode> queue;

	/** path prefixes for checking symbolic link cycles */
	protected PrefixPool pathPrefixHistory, rootPathHistory;

	/** tree's root */
	protected IResource root;

	/**
	 * The root must only be a file or a folder.
	 */
	public UnifiedTree(IResource root) {
		setRoot(root);
	}

	/**
	 * Pass in a a root for the tree, a file tree containing all of the entries for this
	 * tree and a flag indicating whether the UnifiedTree should consult the fileTree where
	 * possible for entries
	 * @param root
	 * @param fileTree
	 */
	public UnifiedTree(IResource root, IFileTree fileTree) {
		this(root);
		this.fileTree = fileTree;
	}

	public void accept(IUnifiedTreeVisitor visitor) throws CoreException {
		accept(visitor, IResource.DEPTH_INFINITE);
	}

	/**
	 * Performs a breadth-first traversal of the unified tree, passing each
	 * node to the provided visitor.
	 */
	public void accept(IUnifiedTreeVisitor visitor, int depth) throws CoreException {
		Assert.isNotNull(root);
		initializeQueue();
		setLevel(0, depth);
		while (!queue.isEmpty()) {
			UnifiedTreeNode node = queue.remove();
			if (isChildrenMarker(node))
				continue;
			if (isLevelMarker(node)) {
				if (!setLevel(getLevel() + 1, depth))
					break;
				continue;
			}
			if (visitor.visit(node))
				addNodeChildrenToQueue(node);
			else
				removeNodeChildrenFromQueue(node);
			//allow reuse of the node, but don't let the freeNodes list grow infinitely
			if (freeNodes.size() < 32767) {
				//free memory-consuming elements of the node for garbage collection
				node.releaseForGc();
				freeNodes.add(node);
			}
			//else, the whole node will be garbage collected since there is no
			//reference to it any more.
		}
	}

	protected void addChildren(UnifiedTreeNode node) {
		Resource parent = (Resource) node.getResource();

		// is there a possibility to have children?
		int parentType = parent.getType();
		if (parentType == IResource.FILE && !node.isFolder())
			return;

		//don't refresh resources in closed or non-existent projects
		if (!parent.getProject().isAccessible())
			return;

		// get the list of resources in the file system
		// don't ask for local children if we know it doesn't exist locally
		IFileInfo[] list = node.existsInFileSystem() ? getLocalList(node) : NO_CHILDREN;
		int localIndex = 0;

		// See if the children of this resource have been computed before
		ResourceInfo resourceInfo = parent.getResourceInfo(false, false);
		int flags = parent.getFlags(resourceInfo);
		boolean unknown = ResourceInfo.isSet(flags, ICoreConstants.M_CHILDREN_UNKNOWN);

		// get the list of resources in the workspace
		if (!unknown && (parentType == IResource.FOLDER || parentType == IResource.PROJECT) && parent.exists(flags, true)) {
			IResource target = null;
			UnifiedTreeNode child = null;
			IResource[] members;
			try {
				members = ((IContainer) parent).members(IContainer.INCLUDE_TEAM_PRIVATE_MEMBERS | IContainer.INCLUDE_HIDDEN);
			} catch (CoreException e) {
				members = NO_RESOURCES;
			}
			int workspaceIndex = 0;
			//iterate simultaneously over file system and workspace members
			while (workspaceIndex < members.length) {
				target = members[workspaceIndex];
				String name = target.getName();
				IFileInfo localInfo = localIndex < list.length ? list[localIndex] : null;
				int comp = localInfo != null ? name.compareTo(localInfo.getName()) : -1;
				//special handling for linked resources
				if (target.isLinked()) {
					//child will be null if location is undefined
					child = createChildForLinkedResource(target);
					workspaceIndex++;
					//if there is a matching local file, skip it - it will be blocked by the linked resource
					if (comp == 0)
						localIndex++;
				} else if (comp == 0) {
					// resource exists in workspace and file system --> localInfo is non-null
					//create workspace-only node for symbolic link that creates a cycle
					if (localInfo.getAttribute(EFS.ATTRIBUTE_SYMLINK) && localInfo.isDirectory() && isRecursiveLink(node.getStore(), localInfo))
						child = createNode(target, null, null, true);
					else
						child = createNode(target, null, localInfo, true);
					localIndex++;
					workspaceIndex++;
				} else if (comp > 0) {
					// resource exists only in file system
					//don't create a node for symbolic links that create a cycle
					if (localInfo.getAttribute(EFS.ATTRIBUTE_SYMLINK) && localInfo.isDirectory() && isRecursiveLink(node.getStore(), localInfo))
						child = null;
					else
						child = createChildNodeFromFileSystem(node, localInfo);
					localIndex++;
				} else {
					// resource exists only in the workspace
					child = createNode(target, null, null, true);
					workspaceIndex++;
				}
				if (child != null)
					addChildToTree(node, child);
			}
		}

		/* process any remaining resource from the file system */
		addChildrenFromFileSystem(node, list, localIndex);

		/* Mark the children as now known */
		if (unknown) {
			// Don't open the info - we might not be inside a workspace-modifying operation
			resourceInfo = parent.getResourceInfo(false, false);
			if (resourceInfo != null)
				resourceInfo.clear(ICoreConstants.M_CHILDREN_UNKNOWN);
		}

		/* if we added children, add the childMarker separator */
		if (node.getFirstChild() != null)
			addChildrenMarker();
	}

	protected void addChildrenFromFileSystem(UnifiedTreeNode node, IFileInfo[] childInfos, int index) {
		if (childInfos == null)
			return;
		for (int i = index; i < childInfos.length; i++) {
			IFileInfo info = childInfos[i];
			//don't create a node for symbolic links that create a cycle
			if (!info.getAttribute(EFS.ATTRIBUTE_SYMLINK) || !info.isDirectory() || !isRecursiveLink(node.getStore(), info))
				addChildToTree(node, createChildNodeFromFileSystem(node, info));
		}
	}

	protected void addChildrenMarker() {
		addElementToQueue(childrenMarker);
	}

	protected void addChildToTree(UnifiedTreeNode node, UnifiedTreeNode child) {
		if (node.getFirstChild() == null)
			node.setFirstChild(child);
		addElementToQueue(child);
	}

	protected void addElementToQueue(UnifiedTreeNode target) {
		queue.add(target);
	}

	protected void addNodeChildrenToQueue(UnifiedTreeNode node) {
		/* if the first child is not null we already added the children */
		/* If the children won't be at a valid level for the refresh depth, don't bother adding them */
		if (!childLevelValid || node.getFirstChild() != null)
			return;
		addChildren(node);
		if (queue.isEmpty())
			return;
		//if we're about to change levels, then the children just added
		//are the last nodes for their level, so add a level marker to the queue
		UnifiedTreeNode nextNode = queue.peek();
		if (isChildrenMarker(nextNode))
			queue.remove();
		nextNode = queue.peek();
		if (isLevelMarker(nextNode))
			addElementToQueue(levelMarker);
	}

	protected void addRootToQueue() {
		//don't refresh in closed projects
		if (!root.getProject().isAccessible())
			return;
		IFileStore store = ((Resource) root).getStore();
		IFileInfo fileInfo = fileTree != null ? fileTree.getFileInfo(store) : store.fetchInfo();
		UnifiedTreeNode node = createNode(root, store, fileInfo, root.exists());
		if (node.existsInFileSystem() || node.existsInWorkspace())
			addElementToQueue(node);
	}

	/**
	 * Creates a tree node for a resource that is linked in a different file system location.
	 */
	protected UnifiedTreeNode createChildForLinkedResource(IResource target) {
		IFileStore store = ((Resource) target).getStore();
		return createNode(target, store, store.fetchInfo(), true);
	}

	/**
	 * Creates a child node for a location in the file system. Does nothing and returns null if the location does not correspond to a valid file/folder.
	 */
	protected UnifiedTreeNode createChildNodeFromFileSystem(UnifiedTreeNode parent, IFileInfo info) {
		IPath childPath = parent.getResource().getFullPath().append(info.getName());
		int type = info.isDirectory() ? IResource.FOLDER : IResource.FILE;
		IResource target = getWorkspace().newResource(childPath, type);
		return createNode(target, null, info, target.exists());
	}

	/**
	 * Factory method for creating a node for this tree.  If the file exists on
	 * disk, either the parent store or child store can be provided. Providing
	 * only the parent store avoids creation of the child store in cases where
	 * it is not needed. The store object is only needed for directories for
	 * simple file system traversals, so this avoids creating store objects
	 * for all files.
	 */
	protected UnifiedTreeNode createNode(IResource resource, IFileStore store, IFileInfo info, boolean existsWorkspace) {
		//first check for reusable objects
		UnifiedTreeNode node = null;
		int size = freeNodes.size();
		if (size > 0) {
			node = freeNodes.remove(size - 1);
			node.reuse(this, resource, store, info, existsWorkspace);
			return node;
		}
		//none available, so create a new one
		return new UnifiedTreeNode(this, resource, store, info, existsWorkspace);
	}

	protected Iterator<UnifiedTreeNode> getChildren(UnifiedTreeNode node) {
		/* if first child is null we need to add node's children to queue */
		if (node.getFirstChild() == null)
			addNodeChildrenToQueue(node);

		/* if the first child is still null, the node does not have any children */
		if (node.getFirstChild() == null)
			return EMPTY_ITERATOR;

		/* get the index of the first child */
		int index = queue.indexOf(node.getFirstChild());

		/* if we do not have children, just return an empty enumeration */
		if (index == -1)
			return EMPTY_ITERATOR;

		/* create an enumeration with node's children */
		List<UnifiedTreeNode> result = new ArrayList<>(10);
		while (true) {
			UnifiedTreeNode child = queue.get(index);
			if (isChildrenMarker(child))
				break;
			result.add(child);
			if (index == queue.size() - 1) {
				index = 0;
			} else {
				index++;
			}
		}
		return result.iterator();
	}

	protected int getLevel() {
		return level;
	}

	protected IFileInfo[] getLocalList(UnifiedTreeNode node) {
		try {
			final IFileStore store = node.getStore();
			IFileInfo[] list;
			if (fileTree != null && (fileTree.getTreeRoot().equals(store) || fileTree.getTreeRoot().isParentOf(store)))
				list = fileTree.getChildInfos(store);
			else
				list = store.childInfos(EFS.NONE, null);

			if (list == null || list.length == 0)
				return NO_CHILDREN;
			list = ((Resource) node.getResource()).filterChildren(list, false);
			int size = list.length;
			if (size > 1)
				quickSort(list, 0, size - 1);
			return list;
		} catch (CoreException e) {
			//treat failure to access the directory as a non-existent directory
			return NO_CHILDREN;
		}
	}

	protected Workspace getWorkspace() {
		return (Workspace) root.getWorkspace();
	}

	protected void initializeQueue() {
		//initialize the queue
		if (queue == null)
			queue = new LinkedList<>();
		else
			queue.clear();
		//initialize the free nodes list
		if (freeNodes == null)
			freeNodes = new ArrayList<>(100);
		else
			freeNodes.clear();
		addRootToQueue();
		addElementToQueue(levelMarker);
	}

	protected boolean isChildrenMarker(UnifiedTreeNode node) {
		return node == childrenMarker;
	}

	protected boolean isLevelMarker(UnifiedTreeNode node) {
		return node == levelMarker;
	}

	private static class PatternHolder {
		//Initialize-on-demand Holder class to avoid compiling Pattern if never needed
		//Pattern: A UNIX or Windows relative path that just points backward
		private static final String REGEX = Platform.getOS().equals(Platform.OS_WIN32) ? "\\.[.\\\\]*" : "\\.[./]*"; //$NON-NLS-1$ //$NON-NLS-2$
		public static final Pattern TRIVIAL_SYMLINK_PATTERN = Pattern.compile(REGEX);
	}

	/**
	 * Initialize history stores for symbolic links.
	 * This may be done when starting a visitor, or later on demand.
	 */
	protected void initLinkHistoriesIfNeeded() {
		if (pathPrefixHistory == null) {
			//Bug 232426: Check what life cycle we need for the histories
			Job job = Job.getJobManager().currentJob();
			if (job instanceof RefreshJob) {
				//we are running from the RefreshJob: use the path history of the job
				RefreshJob refreshJob = (RefreshJob) job;
				pathPrefixHistory = refreshJob.getPathPrefixHistory();
				rootPathHistory = refreshJob.getRootPathHistory();
			} else {
				//Local Histories
				pathPrefixHistory = new PrefixPool(20);
				rootPathHistory = new PrefixPool(20);
			}
		}
		if (rootPathHistory.size() == 0) {
			//add current root to history
			IFileStore rootStore = ((Resource) root).getStore();
			try {
				java.io.File rootFile = rootStore.toLocalFile(EFS.NONE, null);
				if (rootFile != null) {
					IPath rootProjPath = root.getProject().getLocation();
					if (rootProjPath != null) {
						try {
							java.io.File rootProjFile = new java.io.File(rootProjPath.toOSString());
							rootPathHistory.insertShorter(rootProjFile.getCanonicalPath() + '/');
						} catch (IOException ioe) {
							/*ignore here*/
						}
					}
					rootPathHistory.insertShorter(rootFile.getCanonicalPath() + '/');
				}
			} catch (CoreException e) {
				/*ignore*/
			} catch (IOException e) {
				/*ignore*/
			}
		}
	}

	/**
	 * Check if the given child represents a recursive symbolic link.
	 * <p>
	 * On remote EFS stores, this check is not exhaustive and just
	 * finds trivial recursive symbolic links pointing up in the tree.
	 * </p><p>
	 * On local stores, where {@link java.io.File#getCanonicalPath()}
	 * is available, the test is exhaustive but may also find some
	 * false positives with transitive symbolic links. This may lead
	 * to suppressing duplicates of already known resources in the
	 * tree, but it will never lead to not finding a resource at
	 * all. See bug 105554 for details.
	 * </p>
	 * @param parentStore EFS IFileStore representing the parent folder
	 * @param localInfo child representing a symbolic link
	 * @return <code>true</code> if the given child represents a
	 *     recursive symbolic link.
	 */
	private boolean isRecursiveLink(IFileStore parentStore, IFileInfo localInfo) {
		//Try trivial pattern first - works also on remote EFS stores
		String linkTarget = localInfo.getStringAttribute(EFS.ATTRIBUTE_LINK_TARGET);
		if (linkTarget != null && PatternHolder.TRIVIAL_SYMLINK_PATTERN.matcher(linkTarget).matches()) {
			return true;
		}
		if (disable_advanced_recursive_link_checks) {
			// Skip advanced link checking, see bug 537449
			return false;
		}
		//Need canonical paths to check all other possibilities
		try {
			java.io.File parentFile = parentStore.toLocalFile(EFS.NONE, null);
			//If this store cannot be represented as a local file, there is nothing we can do
			//In the future, we could try to resolve the link target
			//against the remote file system to do more checks.
			if (parentFile == null)
				return false;
			//get canonical path for both child and parent
			java.io.File childFile = new java.io.File(parentFile, localInfo.getName());
			String parentPath = parentFile.toPath().toRealPath().toString() + java.io.File.separatorChar;
			String childPath = childFile.toPath().toRealPath().toString() + java.io.File.separatorChar;
			//get or instantiate the prefix and root path histories.
			//Might be done earlier - for now, do it on demand.
			initLinkHistoriesIfNeeded();
			//insert the parent for checking loops
			pathPrefixHistory.insertLonger(parentPath);
			if (pathPrefixHistory.containsAsPrefix(childPath)) {
				//found a potential loop: is it spanning up a new tree?
				if (!rootPathHistory.insertShorter(childPath)) {
					//not spanning up a new tree, so it is a real loop.
					return true;
				}
			} else if (rootPathHistory.hasPrefixOf(childPath)) {
				//child points into a different portion of the tree that we visited already before, or will certainly visit.
				//This does not introduce a loop yet, but introduces duplicate resources.
				//TODO Ideally, such duplicates should be modelled as linked resources. See bug 105534
				return false;
			} else {
				//child neither introduces a loop nor points to a known tree.
				//It probably spans up a new tree of potential prefixes.
				rootPathHistory.insertShorter(childPath);
			}
		} catch (IOException e) {
			//ignore
		} catch (CoreException e) {
			//ignore
		}
		return false;
	}

	protected boolean isValidLevel(int currentLevel, int depth) {
		switch (depth) {
			case IResource.DEPTH_INFINITE :
				return true;
			case IResource.DEPTH_ONE :
				return currentLevel <= 1;
			case IResource.DEPTH_ZERO :
				return currentLevel == 0;
			default :
				return currentLevel + 1000 <= depth;
		}
	}

	/**
	 * Sorts the given array of strings in place.  This is
	 * not using the sorting framework to avoid casting overhead.
	 */
	protected void quickSort(IFileInfo[] infos, int left, int right) {
		int originalLeft = left;
		int originalRight = right;
		IFileInfo mid = infos[(left + right) / 2];
		do {
			while (mid.compareTo(infos[left]) > 0)
				left++;
			while (infos[right].compareTo(mid) > 0)
				right--;
			if (left <= right) {
				IFileInfo tmp = infos[left];
				infos[left] = infos[right];
				infos[right] = tmp;
				left++;
				right--;
			}
		} while (left <= right);
		if (originalLeft < right)
			quickSort(infos, originalLeft, right);
		if (left < originalRight)
			quickSort(infos, left, originalRight);
		return;
	}

	/**
	 * Remove from the last element of the queue to the first child of the
	 * given node.
	 */
	protected void removeNodeChildrenFromQueue(UnifiedTreeNode node) {
		UnifiedTreeNode first = node.getFirstChild();
		if (first == null)
			return;
		while (true) {
			if (first.equals(queue.pollLast()))
				break;
		}
		node.setFirstChild(null);
	}

	/**
	 * Increases the current tree level by one. Returns true if the new
	 * level is still valid for the given depth
	 */
	protected boolean setLevel(int newLevel, int depth) {
		level = newLevel;
		childLevelValid = isValidLevel(level + 1, depth);
		return isValidLevel(level, depth);
	}

	private void setRoot(IResource root) {
		this.root = root;
	}
}

Back to the top