Copyright (c) 2000, 2015 IBM Corporation and others. This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0 which accompanies this distribution, and is available at https://www.eclipse.org/legal/epl-2.0/ SPDX-License-Identifier: EPL-2.0 Contributors: IBM Corporation - initial API and implementation Stephan Herrmann - inconsistent initialization of classpath container backed by external class folder, see https://bugs.eclipse.org/320618 Thirumala Reddy Mutchukota - Contribution to bug: https://bugs.eclipse.org/bugs/show_bug.cgi?id=411423 Terry Parker - [performance] Low hit rates in JavaModel caches - https://bugs.eclipse.org/421165 Andrey Loskutov - ExternalFoldersManager.RefreshJob interrupts auto build job - https://bugs.eclipse.org/476059
/******************************************************************************* * Copyright (c) 2000, 2015 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 * which accompanies this distribution, and is available at * https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 * * Contributors: * IBM Corporation - initial API and implementation * Stephan Herrmann <stephan@cs.tu-berlin.de> - inconsistent initialization of classpath container backed by external class folder, see https://bugs.eclipse.org/320618 * Thirumala Reddy Mutchukota <thirumala@google.com> - Contribution to bug: https://bugs.eclipse.org/bugs/show_bug.cgi?id=411423 * Terry Parker <tparker@google.com> - [performance] Low hit rates in JavaModel caches - https://bugs.eclipse.org/421165 * Andrey Loskutov <loskutov@gmx.de> - ExternalFoldersManager.RefreshJob interrupts auto build job - https://bugs.eclipse.org/476059 *******************************************************************************/
package org.eclipse.jdt.internal.core; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IProjectDescription; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceStatus; import org.eclipse.core.resources.IWorkspace; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.resources.WorkspaceJob; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.MultiStatus; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.jdt.core.IClasspathEntry; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.internal.core.DeltaProcessor.RootInfo; import org.eclipse.jdt.internal.core.util.Messages; import org.eclipse.jdt.internal.core.util.Util; public class ExternalFoldersManager { private static final boolean WINDOWS = System.getProperty("os.name").toLowerCase().contains("windows"); //$NON-NLS-1$//$NON-NLS-2$ private static final String EXTERNAL_PROJECT_NAME = ".org.eclipse.jdt.core.external.folders"; //$NON-NLS-1$ private static final String LINKED_FOLDER_NAME = ".link"; //$NON-NLS-1$ private Map<IPath, IFolder> folders; private Set<IPath> pendingFolders; // subset of keys of 'folders', for which linked folders haven't been created yet. private final AtomicInteger counter = new AtomicInteger(0); /* Singleton instance */ private static ExternalFoldersManager MANAGER; private RefreshJob refreshJob; private ExternalFoldersManager() { // Prevent instantiation // https://bugs.eclipse.org/bugs/show_bug.cgi?id=377806 if (Platform.isRunning()) { /* * The code here runs during JavaCore start-up. * So if we need to open the external folders project, we do this from a job. * Otherwise workspace jobs that attempt to access JDT core functionality can cause a deadlock. * * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=542860. */ class InitializeFolders extends WorkspaceJob { public InitializeFolders() { super("Initialize external folders"); //$NON-NLS-1$ } @Override public IStatus runInWorkspace(IProgressMonitor monitor) { getFolders(); return Status.OK_STATUS; } @Override public boolean belongsTo(Object family) { return family == InitializeFolders.class; } } InitializeFolders initializeFolders = new InitializeFolders(); IProject project = getExternalFoldersProject(); initializeFolders.setRule(project); initializeFolders.schedule(); } } public static synchronized ExternalFoldersManager getExternalFoldersManager() { if (MANAGER == null) { MANAGER = new ExternalFoldersManager(); } return MANAGER; }
Returns a set of external paths to external folders referred to on the given classpath. Returns null if there are none.
/** * Returns a set of external paths to external folders referred to on the given classpath. * Returns <code>null</code> if there are none. */
public static Set<IPath> getExternalFolders(IClasspathEntry[] classpath) { if (classpath == null) return null; Set<IPath> folders = null; for (int i = 0; i < classpath.length; i++) { IClasspathEntry entry = classpath[i]; if (entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) { IPath entryPath = entry.getPath(); if (isExternalFolderPath(entryPath)) { if (folders == null) folders = new LinkedHashSet<>(); folders.add(entryPath); } IPath attachmentPath = entry.getSourceAttachmentPath(); if (isExternalFolderPath(attachmentPath)) { if (folders == null) folders = new LinkedHashSet<>(); folders.add(attachmentPath); } } } return folders; }
Returns true if the provided path is a folder external to the project. The path is expected to be one matching the IClasspathEntry.CPE_LIBRARY case in IClasspathEntry.getPath() definition.
/** * Returns <code>true</code> if the provided path is a folder external to the project. * The path is expected to be one matching the {@link IClasspathEntry#CPE_LIBRARY} case in * {@link IClasspathEntry#getPath()} definition. */
public static boolean isExternalFolderPath(IPath externalPath) { if (externalPath == null || externalPath.isEmpty()) { return false; } JavaModelManager manager = JavaModelManager.getJavaModelManager(); if (manager.isExternalFile(externalPath) || manager.isAssumedExternalFile(externalPath)) { return false; } if (!externalPath.isAbsolute() || (WINDOWS && (externalPath.getDevice() == null && !externalPath.isUNC()))) { // can be only project relative path return false; } // Test if this an absolute path in local file system (not the workspace path) File externalFolder = externalPath.toFile(); if (Files.isRegularFile(externalFolder.toPath())) { manager.addExternalFile(externalPath); return false; } if (Files.isDirectory(externalFolder.toPath())) { return true; } // this can be now only full workspace path or an external path to a not existing file or folder if (isInternalFilePath(externalPath)) { return false; } if (isInternalContainerPath(externalPath)) { return false; } // From here on the legacy code assumes that not existing resource must be external. // We just follow the old assumption. if (externalPath.getFileExtension() != null/*likely a .jar, .zip, .rar or other file*/) { manager.addAssumedExternalFile(externalPath); // assume not existing external (?) file (?) (can also be a folder with dotted name!) return false; } // assume not existing external (?) folder (?) return true; }
Params:
  • path – full absolute workspace path
/** * @param path full absolute workspace path */
private static boolean isInternalFilePath(IPath path) { IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot(); // in case this is full workspace path it should start with project segment if(path.segmentCount() > 1 && wsRoot.getFile(path).exists()) { return true; } return false; }
Params:
  • path – full absolute workspace path
/** * @param path full absolute workspace path */
private static boolean isInternalContainerPath(IPath path) { IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot(); // in case this is full workspace path it should start with project segment int segmentCount = path.segmentCount(); if(segmentCount == 1 && wsRoot.getProject(path.segment(0)).exists()) { return true; } if(segmentCount > 1 && wsRoot.getFolder(path).exists()) { return true; } return false; } public static boolean isInternalPathForExternalFolder(IPath resourcePath) { return EXTERNAL_PROJECT_NAME.equals(resourcePath.segment(0)); } public IFolder addFolder(IPath externalFolderPath, boolean scheduleForCreation) { return addFolder(externalFolderPath, getExternalFoldersProject(), scheduleForCreation); } private IFolder addFolder(IPath externalFolderPath, IProject externalFoldersProject, boolean scheduleForCreation) { Map<IPath, IFolder> knownFolders = getFolders(); IFolder existing; synchronized (this) { existing = knownFolders.get(externalFolderPath); if (existing != null) { return existing; } } IFolder result; do { result = externalFoldersProject.getFolder(LINKED_FOLDER_NAME + this.counter.incrementAndGet()); } while (result.exists()); synchronized (this) { if (scheduleForCreation) { if (this.pendingFolders == null) this.pendingFolders = new LinkedHashSet<>(); this.pendingFolders.add(externalFolderPath); } existing = knownFolders.get(externalFolderPath); if (existing != null) { return existing; } knownFolders.put(externalFolderPath, result); } return result; }
Try to remove the argument from the list of folders pending for creation.
Params:
  • externalPath – to link to
Returns:true if the argument was found in the list of pending folders and could be removed from it.
/** * Try to remove the argument from the list of folders pending for creation. * @param externalPath to link to * @return true if the argument was found in the list of pending folders and could be removed from it. */
public synchronized boolean removePendingFolder(Object externalPath) { if (this.pendingFolders == null) return false; return this.pendingFolders.remove(externalPath); } public IFolder createLinkFolder(IPath externalFolderPath, boolean refreshIfExistAlready, IProgressMonitor monitor) throws CoreException { IProject externalFoldersProject = createExternalFoldersProject(monitor); // run outside synchronized as this can create a resource return createLinkFolder(externalFolderPath, refreshIfExistAlready, externalFoldersProject, monitor); } private IFolder createLinkFolder(IPath externalFolderPath, boolean refreshIfExistAlready, IProject externalFoldersProject, IProgressMonitor monitor) throws CoreException { IFolder result = addFolder(externalFolderPath, externalFoldersProject, false); if (!result.exists()) { try { result.createLink(externalFolderPath, IResource.ALLOW_MISSING_LOCAL, monitor); } catch (CoreException e) { // If we managed to create the folder in the meantime, don't complain if (!result.exists()) { throw e; } } } else if (refreshIfExistAlready) { result.refreshLocal(IResource.DEPTH_INFINITE, monitor); } return result; } public void createPendingFolders(IProgressMonitor monitor) throws JavaModelException{ synchronized (this) { if (this.pendingFolders == null || this.pendingFolders.isEmpty()) return; } IProject externalFoldersProject = null; try { externalFoldersProject = createExternalFoldersProject(monitor); } catch(CoreException e) { throw new JavaModelException(e); } // https://bugs.eclipse.org/bugs/show_bug.cgi?id=368152 // To avoid race condition (from addFolder and removeFolder, load the map elements into an array and clear the map immediately. // The createLinkFolder being in the synchronized block can cause a deadlock and hence keep it out of the synchronized block. Object[] arrayOfFolders = null; synchronized (this) { arrayOfFolders = this.pendingFolders.toArray(); this.pendingFolders.clear(); } for (int i=0; i < arrayOfFolders.length; i++) { try { createLinkFolder((IPath) arrayOfFolders[i], false, externalFoldersProject, monitor); } catch (CoreException e) { Util.log(e, "Error while creating a link for external folder :" + arrayOfFolders[i]); //$NON-NLS-1$ } } } public void cleanUp(IProgressMonitor monitor) throws CoreException { List<Entry<IPath, IFolder>> toDelete = getFoldersToCleanUp(monitor); if (toDelete == null) return; for (Entry<IPath, IFolder> entry : toDelete) { IFolder folder = entry.getValue(); folder.delete(true, monitor); IPath key = entry.getKey(); this.folders.remove(key); } IProject project = getExternalFoldersProject(); if (project.isAccessible() && project.members().length == 1/*remaining member is .project*/) project.delete(true, monitor); } private List<Entry<IPath, IFolder>> getFoldersToCleanUp(IProgressMonitor monitor) throws CoreException { DeltaProcessingState state = JavaModelManager.getDeltaState(); Map<IPath, RootInfo> roots = state.roots; Map<IPath, IPath> sourceAttachments = state.sourceAttachments; if (roots == null && sourceAttachments == null) return null; Map<IPath, IFolder> knownFolders = getFolders(); List<Entry<IPath, IFolder>> result = null; synchronized (knownFolders) { Iterator<Entry<IPath, IFolder>> iterator = knownFolders.entrySet().iterator(); while (iterator.hasNext()) { Entry<IPath, IFolder> entry = iterator.next(); IPath path = entry.getKey(); if ((roots != null && !roots.containsKey(path)) && (sourceAttachments != null && !sourceAttachments.containsKey(path))) { if (entry.getValue() != null) { if (result == null) result = new ArrayList<>(); result.add(entry); } } } } return result; } public IProject getExternalFoldersProject() { return ResourcesPlugin.getWorkspace().getRoot().getProject(EXTERNAL_PROJECT_NAME); } public IProject createExternalFoldersProject(IProgressMonitor monitor) throws CoreException { IProject project = getExternalFoldersProject(); if (!project.isAccessible()) { if (!project.exists()) { createExternalFoldersProject(project, monitor); } openExternalFoldersProject(project, monitor); } return project; } /* * Attempt to open the given project (assuming it exists). * If failing to open, make all attempts to recreate the missing pieces. */ private void openExternalFoldersProject(IProject project, IProgressMonitor monitor) throws CoreException { try { project.open(monitor); } catch (CoreException e1) { if (e1.getStatus().getCode() == IResourceStatus.FAILED_READ_METADATA) { // workspace was moved // (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=241400 and https://bugs.eclipse.org/bugs/show_bug.cgi?id=252571 ) project.delete(false/*don't delete content*/, true/*force*/, monitor); createExternalFoldersProject(project, monitor); } else { // .project or folder on disk have been deleted, recreate them IPath stateLocation = JavaCore.getPlugin().getStateLocation(); IPath projectPath = stateLocation.append(EXTERNAL_PROJECT_NAME); try { Files.createDirectories(projectPath.toFile().toPath()); try (FileOutputStream output = new FileOutputStream(projectPath.append(".project").toOSString())){ //$NON-NLS-1$ output.write(( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + //$NON-NLS-1$ "<projectDescription>\n" + //$NON-NLS-1$ " <name>" + EXTERNAL_PROJECT_NAME + "</name>\n" + //$NON-NLS-1$ //$NON-NLS-2$ " <comment></comment>\n" + //$NON-NLS-1$ " <projects>\n" + //$NON-NLS-1$ " </projects>\n" + //$NON-NLS-1$ " <buildSpec>\n" + //$NON-NLS-1$ " </buildSpec>\n" + //$NON-NLS-1$ " <natures>\n" + //$NON-NLS-1$ " </natures>\n" + //$NON-NLS-1$ "</projectDescription>").getBytes()); //$NON-NLS-1$ } } catch (IOException e) { // fallback to re-creating the project project.delete(false/*don't delete content*/, true/*force*/, monitor); createExternalFoldersProject(project, monitor); } } project.open(monitor); } } private void createExternalFoldersProject(IProject project, IProgressMonitor monitor) throws CoreException { IProjectDescription desc = project.getWorkspace().newProjectDescription(project.getName()); IPath stateLocation = JavaCore.getPlugin().getStateLocation(); desc.setLocation(stateLocation.append(EXTERNAL_PROJECT_NAME)); try { project.create(desc, IResource.HIDDEN, monitor); } catch (CoreException e) { // If we managed to create the project in the meantime, don't complain if (!project.exists()) { throw e; } } } public IFolder getFolder(IPath externalFolderPath) { return getFolders().get(externalFolderPath); } Map<IPath, IFolder> getFolders() { if (this.folders == null) { Map<IPath, IFolder> tempFolders = new LinkedHashMap<>(); IProject project = getExternalFoldersProject(); try { if (!project.isAccessible()) { if (project.exists()) { // workspace was moved (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=252571 ) openExternalFoldersProject(project, null/*no progress*/); } else { // if project doesn't exist, do not open and recreate it as it means that there are no external folders return this.folders = Collections.synchronizedMap(tempFolders); } } IResource[] members = project.members(); for (IResource member : members) { if (member.getType() == IResource.FOLDER && member.isLinked() && member.getName().startsWith(LINKED_FOLDER_NAME)) { IPath externalFolderPath = member.getLocation(); tempFolders.put(externalFolderPath, (IFolder) member); } } } catch (CoreException e) { Util.log(e, "Exception while initializing external folders"); //$NON-NLS-1$ } synchronized (this) { if (this.folders == null) { this.folders = Collections.synchronizedMap(tempFolders); } } } return this.folders; } // https://bugs.eclipse.org/bugs/show_bug.cgi?id=313153 // Use the same RefreshJob if the job is still available private synchronized void runRefreshJob(Collection<IPath> paths) { if (paths == null || paths.isEmpty()) { return; } if (this.refreshJob == null) { this.refreshJob = new RefreshJob(); } this.refreshJob.addFoldersToRefresh(paths); } /* * Refreshes the external folders referenced on the classpath of the given source project */ public void refreshReferences(final IProject[] sourceProjects, IProgressMonitor monitor) { IProject externalProject = getExternalFoldersProject(); try { Set<IPath> externalFolders = null; for (int index = 0; index < sourceProjects.length; index++) { if (sourceProjects[index].equals(externalProject)) continue; if (!JavaProject.hasJavaNature(sourceProjects[index])) continue; Set<IPath> foldersInProject = getExternalFolders(((JavaProject) JavaCore.create(sourceProjects[index])).getResolvedClasspath()); if (foldersInProject == null || foldersInProject.size() == 0) continue; if (externalFolders == null) externalFolders = new LinkedHashSet<>(); externalFolders.addAll(foldersInProject); } runRefreshJob(externalFolders); } catch (CoreException e) { Util.log(e, "Exception while refreshing external project"); //$NON-NLS-1$ } } public void refreshReferences(IProject source, IProgressMonitor monitor) { IProject externalProject = getExternalFoldersProject(); if (source.equals(externalProject)) return; if (!JavaProject.hasJavaNature(source)) return; try { Set<IPath> externalFolders = getExternalFolders(((JavaProject) JavaCore.create(source)).getResolvedClasspath()); runRefreshJob(externalFolders); } catch (CoreException e) { Util.log(e, "Exception while refreshing external project"); //$NON-NLS-1$ } } public IFolder removeFolder(IPath externalFolderPath) { return getFolders().remove(externalFolderPath); } static class RefreshJob extends Job { final LinkedHashSet<IPath> externalFolders; RefreshJob(){ super(Messages.refreshing_external_folders); // bug 476059: don't interrupt autobuild by using rule and system flag. setSystem(true); IWorkspace workspace = ResourcesPlugin.getWorkspace(); setRule(workspace.getRuleFactory().refreshRule(workspace.getRoot())); this.externalFolders = new LinkedHashSet<>(); } @Override public boolean belongsTo(Object family) { return family == ResourcesPlugin.FAMILY_MANUAL_REFRESH; } /* * Add the collection of paths to be refreshed to the already * existing set of paths and schedules the job */ public void addFoldersToRefresh(Collection<IPath> paths) { boolean shouldSchedule; synchronized (this.externalFolders) { this.externalFolders.addAll(paths); shouldSchedule = !this.externalFolders.isEmpty(); } if (shouldSchedule) { schedule(); } } @Override protected IStatus run(IProgressMonitor pm) { MultiStatus errors = new MultiStatus(JavaCore.PLUGIN_ID, IStatus.OK, "Exception while refreshing external folders", null); //$NON-NLS-1$ while (true) { IPath externalPath; synchronized (this.externalFolders) { if (this.externalFolders.isEmpty()) { return errors.isOK()? Status.OK_STATUS : errors; } // keep the path in the list to avoid re-adding it while we are working externalPath = this.externalFolders.iterator().next(); } try { IFolder folder = getExternalFoldersManager().getFolder(externalPath); // https://bugs.eclipse.org/bugs/show_bug.cgi?id=321358 if (folder != null) { folder.refreshLocal(IResource.DEPTH_INFINITE, pm); } } catch (CoreException e) { errors.merge(e.getStatus()); } finally { // we should always remove the path to avoid endless loop trying to refresh it synchronized (this.externalFolders) { this.externalFolders.remove(externalPath); } } } } } }