| /******************************************************************************* |
| * Copyright (c) 2000, 2017 IBM Corporation and others. |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Public License v. 2.0 which is available at |
| * http://www.eclipse.org/legal/epl-2.0. |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.dltk.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.HashSet; |
| 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.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.dltk.annotations.Nullable; |
| import org.eclipse.dltk.core.DLTKCore; |
| import org.eclipse.dltk.core.IBuildpathEntry; |
| import org.eclipse.dltk.core.IScriptProjectFilenames; |
| import org.eclipse.dltk.core.ModelException; |
| import org.eclipse.dltk.core.environment.EnvironmentPathUtils; |
| import org.eclipse.dltk.internal.core.DeltaProcessor.RootInfo; |
| import org.eclipse.dltk.internal.core.util.Util; |
| |
| public class ExternalFoldersManager { |
| private static final String EXTERNAL_PROJECT_NAME = ".org.eclipse.dltk.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; |
| 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 DLKTCore 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 DLTK 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 ExternalFoldersManager getExternalFoldersManager() { |
| if (MANAGER == null) { |
| synchronized (ExternalFoldersManager.class) { |
| if (MANAGER == null) { |
| MANAGER = new ExternalFoldersManager(); |
| } |
| } |
| } |
| return MANAGER; |
| } |
| |
| /* |
| * Returns a set of external path to external folders referred to on the given |
| * buildpath. Returns null if none. |
| */ |
| @Nullable |
| public static Set<IPath> getExternalFolders(IBuildpathEntry[] buildpath) { |
| if (buildpath == null) |
| return null; |
| Set<IPath> folders = null; |
| for (int i = 0; i < buildpath.length; i++) { |
| IBuildpathEntry entry = buildpath[i]; |
| if (entry.getEntryKind() == IBuildpathEntry.BPE_LIBRARY) { |
| IPath entryPath = entry.getPath(); |
| if (EnvironmentPathUtils.isLocalEnvironment(entryPath)) { |
| final IPath local = EnvironmentPathUtils.getLocalPath(entryPath); |
| if (isExternalFolderPath(local)) { |
| if (folders == null) |
| folders = new HashSet<>(); |
| folders.add(local); |
| } |
| } |
| } |
| } |
| return folders; |
| } |
| |
| public static boolean isExternalFolderPath(IPath externalPath) { |
| if (externalPath == null) |
| return false; |
| if (externalPath.segmentCount() > 0 |
| && ResourcesPlugin.getWorkspace().getRoot().getProject(externalPath.segment(0)).exists()) |
| return false; |
| File externalFolder = externalPath.toFile(); |
| if (externalFolder.isFile()) |
| return false; |
| if (externalPath.getFileExtension() != null/* |
| * likely a .jar, .zip, .rar or other file |
| */ |
| && !externalFolder.exists()) |
| return false; |
| return true; |
| } |
| |
| 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 synchronized 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 ModelException { |
| synchronized (this) { |
| if (this.pendingFolders == null || this.pendingFolders.isEmpty()) |
| return; |
| } |
| |
| IProject externalFoldersProject = null; |
| try { |
| externalFoldersProject = createExternalFoldersProject(monitor); |
| } catch (CoreException e) { |
| throw new ModelException(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 = ModelManager.getModelManager().deltaState; |
| Map<IPath, RootInfo> roots = state.roots; |
| 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))) { |
| 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=537712 and |
| // 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 = DLTKCore.getPlugin().getStateLocation(); |
| IPath projectPath = stateLocation.append(EXTERNAL_PROJECT_NAME); |
| try { |
| Files.createDirectories(projectPath.toFile().toPath()); |
| try (FileOutputStream output = new FileOutputStream( |
| projectPath.append(IScriptProjectFilenames.PROJECT_FILENAME).toOSString())) { |
| 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 = DLTKCore.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 synchronized IFolder getFolder(IPath externalFolderPath) { |
| return getFolders().get(externalFolderPath); |
| } |
| |
| private 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 (!ScriptProject.hasScriptNature(sourceProjects[index])) |
| continue; |
| |
| Set<IPath> foldersInProject = getExternalFolders( |
| ((ScriptProject) DLTKCore.create(sourceProjects[index])).getResolvedBuildpath()); |
| |
| 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 (!ScriptProject.hasScriptNature(source)) |
| return; |
| try { |
| Set<IPath> externalFolders = getExternalFolders( |
| ((ScriptProject) DLTKCore.create(source)).getResolvedBuildpath()); |
| runRefreshJob(externalFolders); |
| } catch (CoreException e) { |
| Util.log(e, "Exception while refreshing external project"); //$NON-NLS-1$ |
| } |
| } |
| |
| public synchronized 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(DLTKCore.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); |
| } |
| } |
| } |
| } |
| } |
| |
| } |