org.eclipse.jdt.internal.core.ExternalFoldersManager.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.jdt.internal.core.ExternalFoldersManager.java

Source

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

}