| /******************************************************************************* |
| * 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 <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 <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; |
| } |
| |
| /** |
| * @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; |
| } |
| |
| /** |
| * @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. |
| * @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); |
| } |
| } |
| } |
| } |
| } |
| |
| } |