text: /*******************************************************************************
   * 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.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()) {
  			getFolders();
  		}
  	}
  
  	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)) {
  			// 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);
  	}
  
  	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$
  			}
  			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);
  					}
  				}
  			}
  		}
  	}
  
  }
  