1 /*******************************************************************************
2  * Copyright (c) 2000, 2015 IBM Corporation and others.
3  *
4  * This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License 2.0
6  * which accompanies this distribution, and is available at
7  * https://www.eclipse.org/legal/epl-2.0/
8  *
9  * SPDX-License-Identifier: EPL-2.0
10  *
11  * Contributors:
12  *     IBM Corporation - initial API and implementation
13  *     Stephan Herrmann <stephan@cs.tu-berlin.de> - inconsistent initialization of classpath container backed by external class folder, see https://bugs.eclipse.org/320618
14  *     Thirumala Reddy Mutchukota <thirumala@google.com> - Contribution to bug: https://bugs.eclipse.org/bugs/show_bug.cgi?id=411423
15  *     Terry Parker <tparker@google.com> - [performance] Low hit rates in JavaModel caches - https://bugs.eclipse.org/421165
16  *     Andrey Loskutov <loskutov@gmx.de> - ExternalFoldersManager.RefreshJob interrupts auto build job - https://bugs.eclipse.org/476059
17  *******************************************************************************/
18 package org.eclipse.jdt.internal.core;
19 
20 import java.io.File;
21 import java.io.FileOutputStream;
22 import java.io.IOException;
23 import java.nio.file.Files;
24 import java.util.ArrayList;
25 import java.util.Collection;
26 import java.util.Collections;
27 import java.util.Iterator;
28 import java.util.LinkedHashMap;
29 import java.util.LinkedHashSet;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.Map.Entry;
33 import java.util.Set;
34 import java.util.concurrent.atomic.AtomicInteger;
35 
36 import org.eclipse.core.resources.IFolder;
37 import org.eclipse.core.resources.IProject;
38 import org.eclipse.core.resources.IProjectDescription;
39 import org.eclipse.core.resources.IResource;
40 import org.eclipse.core.resources.IResourceStatus;
41 import org.eclipse.core.resources.IWorkspace;
42 import org.eclipse.core.resources.IWorkspaceRoot;
43 import org.eclipse.core.resources.ResourcesPlugin;
44 import org.eclipse.core.resources.WorkspaceJob;
45 import org.eclipse.core.runtime.CoreException;
46 import org.eclipse.core.runtime.IPath;
47 import org.eclipse.core.runtime.IProgressMonitor;
48 import org.eclipse.core.runtime.IStatus;
49 import org.eclipse.core.runtime.MultiStatus;
50 import org.eclipse.core.runtime.Platform;
51 import org.eclipse.core.runtime.Status;
52 import org.eclipse.core.runtime.jobs.Job;
53 import org.eclipse.jdt.core.IClasspathEntry;
54 import org.eclipse.jdt.core.JavaCore;
55 import org.eclipse.jdt.core.JavaModelException;
56 import org.eclipse.jdt.internal.core.DeltaProcessor.RootInfo;
57 import org.eclipse.jdt.internal.core.util.Messages;
58 import org.eclipse.jdt.internal.core.util.Util;
59 
60 public class ExternalFoldersManager {
61 	private static final boolean WINDOWS = System.getProperty("os.name").toLowerCase().contains("windows");  //$NON-NLS-1$//$NON-NLS-2$
62 	private static final String EXTERNAL_PROJECT_NAME = ".org.eclipse.jdt.core.external.folders"; //$NON-NLS-1$
63 	private static final String LINKED_FOLDER_NAME = ".link"; //$NON-NLS-1$
64 	private Map<IPath, IFolder> folders;
65 	private Set<IPath> pendingFolders; // subset of keys of 'folders', for which linked folders haven't been created yet.
66 	private final AtomicInteger counter = new AtomicInteger(0);
67 	/* Singleton instance */
68 	private static ExternalFoldersManager MANAGER;
69 	private RefreshJob refreshJob;
70 
ExternalFoldersManager()71 	private ExternalFoldersManager() {
72 		// Prevent instantiation
73 		// https://bugs.eclipse.org/bugs/show_bug.cgi?id=377806
74 		if (Platform.isRunning()) {
75 			/*
76 			 * The code here runs during JavaCore start-up.
77 			 * So if we need to open the external folders project, we do this from a job.
78 			 * Otherwise workspace jobs that attempt to access JDT core functionality can cause a deadlock.
79 			 *
80 			 * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=542860.
81 			 */
82 			class InitializeFolders extends WorkspaceJob {
83 				public InitializeFolders() {
84 					super("Initialize external folders"); //$NON-NLS-1$
85 				}
86 
87 				@Override
88 				public IStatus runInWorkspace(IProgressMonitor monitor) {
89 					getFolders();
90 					return Status.OK_STATUS;
91 				}
92 
93 				@Override
94 				public boolean belongsTo(Object family) {
95 					return family == InitializeFolders.class;
96 				}
97 			}
98 			InitializeFolders initializeFolders = new InitializeFolders();
99 			IProject project = getExternalFoldersProject();
100 			initializeFolders.setRule(project);
101 			initializeFolders.schedule();
102 		}
103 	}
104 
getExternalFoldersManager()105 	public static synchronized ExternalFoldersManager getExternalFoldersManager() {
106 		if (MANAGER == null) {
107 			 MANAGER = new ExternalFoldersManager();
108 		}
109 		return MANAGER;
110 	}
111 
112 	/**
113 	 * Returns a set of external paths to external folders referred to on the given classpath.
114 	 * Returns <code>null</code> if there are none.
115 	 */
getExternalFolders(IClasspathEntry[] classpath)116 	public static Set<IPath> getExternalFolders(IClasspathEntry[] classpath) {
117 		if (classpath == null)
118 			return null;
119 		Set<IPath> folders = null;
120 		for (int i = 0; i < classpath.length; i++) {
121 			IClasspathEntry entry = classpath[i];
122 			if (entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) {
123 				IPath entryPath = entry.getPath();
124 				if (isExternalFolderPath(entryPath)) {
125 					if (folders == null)
126 						folders = new LinkedHashSet<>();
127 					folders.add(entryPath);
128 				}
129 				IPath attachmentPath = entry.getSourceAttachmentPath();
130 				if (isExternalFolderPath(attachmentPath)) {
131 					if (folders == null)
132 						folders = new LinkedHashSet<>();
133 					folders.add(attachmentPath);
134 				}
135 			}
136 		}
137 		return folders;
138 	}
139 
140 	/**
141 	 * Returns <code>true</code> if the provided path is a folder external to the project.
142 	 * The path is expected to be one matching the {@link IClasspathEntry#CPE_LIBRARY} case in
143 	 * {@link IClasspathEntry#getPath()} definition.
144 	 */
isExternalFolderPath(IPath externalPath)145 	public static boolean isExternalFolderPath(IPath externalPath) {
146 		if (externalPath == null || externalPath.isEmpty()) {
147 			return false;
148 		}
149 
150 		JavaModelManager manager = JavaModelManager.getJavaModelManager();
151 		if (manager.isExternalFile(externalPath) || manager.isAssumedExternalFile(externalPath)) {
152 			return false;
153 		}
154 		if (!externalPath.isAbsolute()
155 				|| (WINDOWS && (externalPath.getDevice() == null && !externalPath.isUNC()))) {
156 			// can be only project relative path
157 			return false;
158 		}
159 		// Test if this an absolute path in local file system (not the workspace path)
160 		File externalFolder = externalPath.toFile();
161 		if (Files.isRegularFile(externalFolder.toPath())) {
162 			manager.addExternalFile(externalPath);
163 			return false;
164 		}
165 		if (Files.isDirectory(externalFolder.toPath())) {
166 			return true;
167 		}
168 		// this can be now only full workspace path or an external path to a not existing file or folder
169 		if (isInternalFilePath(externalPath)) {
170 			return false;
171 		}
172 		if (isInternalContainerPath(externalPath)) {
173 			return false;
174 		}
175 		// From here on the legacy code assumes that not existing resource must be external.
176 		// We just follow the old assumption.
177 		if (externalPath.getFileExtension() != null/*likely a .jar, .zip, .rar or other file*/) {
178 			manager.addAssumedExternalFile(externalPath);
179 			// assume not existing external (?) file (?) (can also be a folder with dotted name!)
180 			return false;
181 		}
182 		// assume not existing external (?) folder (?)
183 		return true;
184 	}
185 
186 	/**
187 	 * @param path full absolute workspace path
188 	 */
isInternalFilePath(IPath path)189 	private static boolean isInternalFilePath(IPath path) {
190 		IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot();
191 		// in case this is full workspace path it should start with project segment
192 		if(path.segmentCount() > 1 && wsRoot.getFile(path).exists()) {
193 			return true;
194 		}
195 		return false;
196 	}
197 
198 	/**
199 	 * @param path full absolute workspace path
200 	 */
isInternalContainerPath(IPath path)201 	private static boolean isInternalContainerPath(IPath path) {
202 		IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot();
203 		// in case this is full workspace path it should start with project segment
204 		int segmentCount = path.segmentCount();
205 		if(segmentCount == 1 && wsRoot.getProject(path.segment(0)).exists()) {
206 			return true;
207 		}
208 		if(segmentCount > 1 && wsRoot.getFolder(path).exists()) {
209 			return true;
210 		}
211 		return false;
212 	}
213 
isInternalPathForExternalFolder(IPath resourcePath)214 	public static boolean isInternalPathForExternalFolder(IPath resourcePath) {
215 		return EXTERNAL_PROJECT_NAME.equals(resourcePath.segment(0));
216 	}
217 
addFolder(IPath externalFolderPath, boolean scheduleForCreation)218 	public IFolder addFolder(IPath externalFolderPath, boolean scheduleForCreation) {
219 		return addFolder(externalFolderPath, getExternalFoldersProject(), scheduleForCreation);
220 	}
221 
addFolder(IPath externalFolderPath, IProject externalFoldersProject, boolean scheduleForCreation)222 	private IFolder addFolder(IPath externalFolderPath, IProject externalFoldersProject, boolean scheduleForCreation) {
223 		Map<IPath, IFolder> knownFolders = getFolders();
224 
225 		IFolder existing;
226 		synchronized (this) {
227 			existing = knownFolders.get(externalFolderPath);
228 			if (existing != null) {
229 				return existing;
230 			}
231 		}
232 
233 		IFolder result;
234 		do {
235 			result = externalFoldersProject.getFolder(LINKED_FOLDER_NAME + this.counter.incrementAndGet());
236 		} while (result.exists());
237 
238 		synchronized (this) {
239 			if (scheduleForCreation) {
240 				if (this.pendingFolders == null)
241 					this.pendingFolders = new LinkedHashSet<>();
242 				this.pendingFolders.add(externalFolderPath);
243 			}
244 			existing = knownFolders.get(externalFolderPath);
245 			if (existing != null) {
246 				return existing;
247 			}
248 			knownFolders.put(externalFolderPath, result);
249 		}
250 		return result;
251 	}
252 
253 	/**
254 	 * Try to remove the argument from the list of folders pending for creation.
255 	 * @param externalPath to link to
256 	 * @return true if the argument was found in the list of pending folders and could be removed from it.
257 	 */
removePendingFolder(Object externalPath)258 	public synchronized boolean removePendingFolder(Object externalPath) {
259 		if (this.pendingFolders == null)
260 			return false;
261 		return this.pendingFolders.remove(externalPath);
262 	}
263 
createLinkFolder(IPath externalFolderPath, boolean refreshIfExistAlready, IProgressMonitor monitor)264 	public IFolder createLinkFolder(IPath externalFolderPath, boolean refreshIfExistAlready, IProgressMonitor monitor) throws CoreException {
265 		IProject externalFoldersProject = createExternalFoldersProject(monitor); // run outside synchronized as this can create a resource
266 		return createLinkFolder(externalFolderPath, refreshIfExistAlready, externalFoldersProject, monitor);
267 	}
268 
createLinkFolder(IPath externalFolderPath, boolean refreshIfExistAlready, IProject externalFoldersProject, IProgressMonitor monitor)269 	private IFolder createLinkFolder(IPath externalFolderPath, boolean refreshIfExistAlready,
270 									IProject externalFoldersProject, IProgressMonitor monitor) throws CoreException {
271 
272 		IFolder result = addFolder(externalFolderPath, externalFoldersProject, false);
273 		if (!result.exists()) {
274 			try {
275 				result.createLink(externalFolderPath, IResource.ALLOW_MISSING_LOCAL, monitor);
276 			} catch (CoreException e) {
277 				// If we managed to create the folder in the meantime, don't complain
278 				if (!result.exists()) {
279 					throw e;
280 				}
281 			}
282 		} else if (refreshIfExistAlready) {
283 			result.refreshLocal(IResource.DEPTH_INFINITE,  monitor);
284 		}
285 		return result;
286 	}
287 
createPendingFolders(IProgressMonitor monitor)288 	public void createPendingFolders(IProgressMonitor monitor) throws JavaModelException{
289 		synchronized (this) {
290 			if (this.pendingFolders == null || this.pendingFolders.isEmpty()) return;
291 		}
292 
293 		IProject externalFoldersProject = null;
294 		try {
295 			externalFoldersProject = createExternalFoldersProject(monitor);
296 		}
297 		catch(CoreException e) {
298 			throw new JavaModelException(e);
299 		}
300 		// https://bugs.eclipse.org/bugs/show_bug.cgi?id=368152
301 		// To avoid race condition (from addFolder and removeFolder, load the map elements into an array and clear the map immediately.
302 		// The createLinkFolder being in the synchronized block can cause a deadlock and hence keep it out of the synchronized block.
303 		Object[] arrayOfFolders = null;
304 		synchronized (this) {
305 			arrayOfFolders = this.pendingFolders.toArray();
306 			this.pendingFolders.clear();
307 		}
308 
309 		for (int i=0; i < arrayOfFolders.length; i++) {
310 			try {
311 				createLinkFolder((IPath) arrayOfFolders[i], false, externalFoldersProject, monitor);
312 			} catch (CoreException e) {
313 				Util.log(e, "Error while creating a link for external folder :" + arrayOfFolders[i]); //$NON-NLS-1$
314 			}
315 		}
316 	}
317 
cleanUp(IProgressMonitor monitor)318 	public void cleanUp(IProgressMonitor monitor) throws CoreException {
319 		List<Entry<IPath, IFolder>> toDelete = getFoldersToCleanUp(monitor);
320 		if (toDelete == null)
321 			return;
322 		for (Entry<IPath, IFolder> entry : toDelete) {
323 			IFolder folder = entry.getValue();
324 			folder.delete(true, monitor);
325 			IPath key = entry.getKey();
326 			this.folders.remove(key);
327 		}
328 		IProject project = getExternalFoldersProject();
329 		if (project.isAccessible() && project.members().length == 1/*remaining member is .project*/)
330 			project.delete(true, monitor);
331 	}
332 
getFoldersToCleanUp(IProgressMonitor monitor)333 	private List<Entry<IPath, IFolder>> getFoldersToCleanUp(IProgressMonitor monitor) throws CoreException {
334 		DeltaProcessingState state = JavaModelManager.getDeltaState();
335 		Map<IPath, RootInfo> roots = state.roots;
336 		Map<IPath, IPath> sourceAttachments = state.sourceAttachments;
337 		if (roots == null && sourceAttachments == null)
338 			return null;
339 		Map<IPath, IFolder> knownFolders = getFolders();
340 		List<Entry<IPath, IFolder>> result = null;
341 		synchronized (knownFolders) {
342 			Iterator<Entry<IPath, IFolder>> iterator = knownFolders.entrySet().iterator();
343 			while (iterator.hasNext()) {
344 				Entry<IPath, IFolder> entry = iterator.next();
345 				IPath path = entry.getKey();
346 				if ((roots != null && !roots.containsKey(path))
347 						&& (sourceAttachments != null && !sourceAttachments.containsKey(path))) {
348 					if (entry.getValue() != null) {
349 						if (result == null)
350 							result = new ArrayList<>();
351 						result.add(entry);
352 					}
353 				}
354 			}
355 		}
356 		return result;
357 	}
358 
getExternalFoldersProject()359 	public IProject getExternalFoldersProject() {
360 		return ResourcesPlugin.getWorkspace().getRoot().getProject(EXTERNAL_PROJECT_NAME);
361 	}
362 
createExternalFoldersProject(IProgressMonitor monitor)363 	public IProject createExternalFoldersProject(IProgressMonitor monitor) throws CoreException {
364 		IProject project = getExternalFoldersProject();
365 		if (!project.isAccessible()) {
366 			if (!project.exists()) {
367 				createExternalFoldersProject(project, monitor);
368 			}
369 			openExternalFoldersProject(project, monitor);
370 		}
371 		return project;
372 	}
373 
374 	/*
375 	 * Attempt to open the given project (assuming it exists).
376 	 * If failing to open, make all attempts to recreate the missing pieces.
377 	 */
openExternalFoldersProject(IProject project, IProgressMonitor monitor)378 	private void openExternalFoldersProject(IProject project, IProgressMonitor monitor) throws CoreException {
379 		try {
380 			project.open(monitor);
381 		} catch (CoreException e1) {
382 			if (e1.getStatus().getCode() == IResourceStatus.FAILED_READ_METADATA) {
383 				// workspace was moved
384 				// (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=241400 and https://bugs.eclipse.org/bugs/show_bug.cgi?id=252571 )
385 				project.delete(false/*don't delete content*/, true/*force*/, monitor);
386 				createExternalFoldersProject(project, monitor);
387 			} else {
388 				// .project or folder on disk have been deleted, recreate them
389 				IPath stateLocation = JavaCore.getPlugin().getStateLocation();
390 				IPath projectPath = stateLocation.append(EXTERNAL_PROJECT_NAME);
391 				try {
392 					Files.createDirectories(projectPath.toFile().toPath());
393 					try (FileOutputStream output = new FileOutputStream(projectPath.append(".project").toOSString())){ //$NON-NLS-1$
394 				        output.write((
395 				        		"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + //$NON-NLS-1$
396 				        		"<projectDescription>\n" + //$NON-NLS-1$
397 				        		"	<name>" + EXTERNAL_PROJECT_NAME + "</name>\n" + //$NON-NLS-1$ //$NON-NLS-2$
398 				        		"	<comment></comment>\n" + //$NON-NLS-1$
399 				        		"	<projects>\n" + //$NON-NLS-1$
400 				        		"	</projects>\n" + //$NON-NLS-1$
401 				        		"	<buildSpec>\n" + //$NON-NLS-1$
402 				        		"	</buildSpec>\n" + //$NON-NLS-1$
403 				        		"	<natures>\n" + //$NON-NLS-1$
404 				        		"	</natures>\n" + //$NON-NLS-1$
405 				        		"</projectDescription>").getBytes()); //$NON-NLS-1$
406 				    }
407 				} catch (IOException e) {
408 					// fallback to re-creating the project
409 					project.delete(false/*don't delete content*/, true/*force*/, monitor);
410 					createExternalFoldersProject(project, monitor);
411 				}
412 			}
413 			project.open(monitor);
414 		}
415 	}
416 
417 
createExternalFoldersProject(IProject project, IProgressMonitor monitor)418 	private void createExternalFoldersProject(IProject project, IProgressMonitor monitor) throws CoreException {
419 		IProjectDescription desc = project.getWorkspace().newProjectDescription(project.getName());
420 		IPath stateLocation = JavaCore.getPlugin().getStateLocation();
421 		desc.setLocation(stateLocation.append(EXTERNAL_PROJECT_NAME));
422 		try {
423 			project.create(desc, IResource.HIDDEN, monitor);
424 		} catch (CoreException e) {
425 			// If we managed to create the project in the meantime, don't complain
426 			if (!project.exists()) {
427 				throw e;
428 			}
429 		}
430 	}
431 
getFolder(IPath externalFolderPath)432 	public IFolder getFolder(IPath externalFolderPath) {
433 		return getFolders().get(externalFolderPath);
434 	}
435 
getFolders()436 	Map<IPath, IFolder> getFolders() {
437 		if (this.folders == null) {
438 			Map<IPath, IFolder> tempFolders = new LinkedHashMap<>();
439 			IProject project = getExternalFoldersProject();
440 			try {
441 				if (!project.isAccessible()) {
442 					if (project.exists()) {
443 						// workspace was moved (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=252571 )
444 						openExternalFoldersProject(project, null/*no progress*/);
445 					} else {
446 						// if project doesn't exist, do not open and recreate it as it means that there are no external folders
447 						return this.folders = Collections.synchronizedMap(tempFolders);
448 					}
449 				}
450 				IResource[] members = project.members();
451 				for (IResource member : members) {
452 					if (member.getType() == IResource.FOLDER && member.isLinked() && member.getName().startsWith(LINKED_FOLDER_NAME)) {
453 						IPath externalFolderPath = member.getLocation();
454 						tempFolders.put(externalFolderPath, (IFolder) member);
455 					}
456 				}
457 			} catch (CoreException e) {
458 				Util.log(e, "Exception while initializing external folders"); //$NON-NLS-1$
459 			}
460 			synchronized (this) {
461 				if (this.folders == null) {
462 					this.folders = Collections.synchronizedMap(tempFolders);
463 				}
464 			}
465 		}
466 		return this.folders;
467 	}
468 
469 	// https://bugs.eclipse.org/bugs/show_bug.cgi?id=313153
470 	// Use the same RefreshJob if the job is still available
runRefreshJob(Collection<IPath> paths)471 	private synchronized void runRefreshJob(Collection<IPath> paths) {
472 		if (paths == null || paths.isEmpty()) {
473 			return;
474 		}
475 		if (this.refreshJob == null) {
476 			this.refreshJob = new RefreshJob();
477 		}
478 		this.refreshJob.addFoldersToRefresh(paths);
479 	}
480 
481 	/*
482 	 * Refreshes the external folders referenced on the classpath of the given source project
483 	 */
refreshReferences(final IProject[] sourceProjects, IProgressMonitor monitor)484 	public void refreshReferences(final IProject[] sourceProjects, IProgressMonitor monitor) {
485 		IProject externalProject = getExternalFoldersProject();
486 		try {
487 			Set<IPath> externalFolders = null;
488 			for (int index = 0; index < sourceProjects.length; index++) {
489 				if (sourceProjects[index].equals(externalProject))
490 					continue;
491 				if (!JavaProject.hasJavaNature(sourceProjects[index]))
492 					continue;
493 
494 				Set<IPath> foldersInProject = getExternalFolders(((JavaProject) JavaCore.create(sourceProjects[index])).getResolvedClasspath());
495 
496 				if (foldersInProject == null || foldersInProject.size() == 0)
497 					continue;
498 				if (externalFolders == null)
499 					externalFolders = new LinkedHashSet<>();
500 
501 				externalFolders.addAll(foldersInProject);
502 			}
503 			runRefreshJob(externalFolders);
504 
505 		} catch (CoreException e) {
506 			Util.log(e, "Exception while refreshing external project"); //$NON-NLS-1$
507 		}
508 	}
509 
refreshReferences(IProject source, IProgressMonitor monitor)510 	public void refreshReferences(IProject source, IProgressMonitor monitor) {
511 		IProject externalProject = getExternalFoldersProject();
512 		if (source.equals(externalProject))
513 			return;
514 		if (!JavaProject.hasJavaNature(source))
515 			return;
516 		try {
517 			Set<IPath> externalFolders = getExternalFolders(((JavaProject) JavaCore.create(source)).getResolvedClasspath());
518 			runRefreshJob(externalFolders);
519 		} catch (CoreException e) {
520 			Util.log(e, "Exception while refreshing external project"); //$NON-NLS-1$
521 		}
522 	}
523 
removeFolder(IPath externalFolderPath)524 	public IFolder removeFolder(IPath externalFolderPath) {
525 		return getFolders().remove(externalFolderPath);
526 	}
527 
528 	static class RefreshJob extends Job {
529 
530 		final LinkedHashSet<IPath> externalFolders;
531 
RefreshJob()532 		RefreshJob(){
533 			super(Messages.refreshing_external_folders);
534 			// bug 476059: don't interrupt autobuild by using rule and system flag.
535 			setSystem(true);
536 			IWorkspace workspace = ResourcesPlugin.getWorkspace();
537 			setRule(workspace.getRuleFactory().refreshRule(workspace.getRoot()));
538 			this.externalFolders = new LinkedHashSet<>();
539 		}
540 
541 		@Override
belongsTo(Object family)542 		public boolean belongsTo(Object family) {
543 			return family == ResourcesPlugin.FAMILY_MANUAL_REFRESH;
544 		}
545 
546 		/*
547 		 * Add the collection of paths to be refreshed to the already
548 		 * existing set of paths and schedules the job
549 		 */
addFoldersToRefresh(Collection<IPath> paths)550 		public void addFoldersToRefresh(Collection<IPath> paths) {
551 			boolean shouldSchedule;
552 			synchronized (this.externalFolders) {
553 				this.externalFolders.addAll(paths);
554 				shouldSchedule = !this.externalFolders.isEmpty();
555 			}
556 			if (shouldSchedule) {
557 				schedule();
558 			}
559 		}
560 
561 		@Override
run(IProgressMonitor pm)562 		protected IStatus run(IProgressMonitor pm) {
563 			MultiStatus errors = new MultiStatus(JavaCore.PLUGIN_ID, IStatus.OK,
564 					"Exception while refreshing external folders", null); //$NON-NLS-1$
565 			while (true) {
566 				IPath externalPath;
567 				synchronized (this.externalFolders) {
568 					if (this.externalFolders.isEmpty()) {
569 						return errors.isOK()? Status.OK_STATUS : errors;
570 					}
571 					// keep the path in the list to avoid re-adding it while we are working
572 					externalPath = this.externalFolders.iterator().next();
573 				}
574 
575 				try {
576 					IFolder folder = getExternalFoldersManager().getFolder(externalPath);
577 					// https://bugs.eclipse.org/bugs/show_bug.cgi?id=321358
578 					if (folder != null) {
579 						folder.refreshLocal(IResource.DEPTH_INFINITE, pm);
580 					}
581 				} catch (CoreException e) {
582 					errors.merge(e.getStatus());
583 				} finally {
584 					// we should always remove the path to avoid endless loop trying to refresh it
585 					synchronized (this.externalFolders) {
586 						this.externalFolders.remove(externalPath);
587 					}
588 				}
589 			}
590 		}
591 	}
592 
593 }
594