blob: 642abc6419d702cd1a298dd4822c37e47d39a162 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2014-2016 Red Hat Inc., and others
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Mickael Istria (Red Hat Inc.) - initial API and implementation
******************************************************************************/
package org.eclipse.ui.internal.wizards.datatransfer;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import org.eclipse.core.resources.IContainer;
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.IWorkspace;
import org.eclipse.core.resources.IWorkspaceDescription;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.JobGroup;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IWorkingSet;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.internal.ide.IDEWorkbenchPlugin;
import org.eclipse.ui.wizards.datatransfer.ProjectConfigurator;
/**
* The {@link SmartImportJob} is a Job to import a given folder or archive. It
* detects the nested projects in the given source and configure projects
* according to the metadata it could find. The behavior is extensible, and
* contributors can add a {@link ProjectConfigurator} strategy via extension
* point to add support for more project kinds.
*
* @since 3.12
*
*/
public class SmartImportJob extends Job {
/*
* Input parameters
*/
private File rootDirectory;
private Set<File> directoriesToImport;
private Set<File> excludedDirectories;
private boolean discardRootProject;
private boolean deepChildrenDetection;
private boolean configureProjects;
private boolean reconfigureEclipseProjects;
private IWorkingSet[] workingSets;
/*
* working fields
*/
private IProject rootProject;
private IWorkspaceRoot workspaceRoot;
private ProjectConfiguratorExtensionManager configurationManager;
private RecursiveImportListener listener;
protected Map<File, List<ProjectConfigurator>> importProposals;
private Map<IProject, List<ProjectConfigurator>> report;
private Map<IPath, Exception> errors;
private JobGroup crawlerJobGroup;
/**
* Builds a new instance of the job
*
* @param rootDirectory
* the root directory to import and analyze
* @param workingSets
* working sets to assign to imported projects
* @param configureProjects
* whether we want to configure projects (natures etc...)
* according to their metadata
* @param recuriveChildrenDetection
* whether to recurse for detection of nested projects
*/
public SmartImportJob(File rootDirectory, Set<IWorkingSet> workingSets, boolean configureProjects, boolean recuriveChildrenDetection) {
super(rootDirectory.getAbsolutePath());
this.workspaceRoot = ResourcesPlugin.getWorkspace().getRoot();
this.rootDirectory = rootDirectory;
if (workingSets != null) {
this.workingSets = workingSets.toArray(new IWorkingSet[workingSets.size()]);
} else {
this.workingSets = new IWorkingSet[0];
}
this.configureProjects = configureProjects;
this.deepChildrenDetection = recuriveChildrenDetection;
this.report = Collections.synchronizedMap(new HashMap<IProject, List<ProjectConfigurator>>());
this.errors = Collections.synchronizedMap(new HashMap<IPath, Exception>());
this.crawlerJobGroup = new JobGroup(DataTransferMessages.SmartImportJob_detectAndConfigureProjects, 0, 1);
}
/**
* @return The root directory for the import operation
*/
public File getRoot() {
return this.rootDirectory;
}
/**
* Sets the directories that have been detected by preliminary detection and that
* user has selected to import. Those will be imported and configured in any case.
* This does not impact output of {@link #getImportProposals(IProgressMonitor)}
* @param directories
*/
public void setDirectoriesToImport(Set<File> directories) {
this.directoriesToImport = directories;
}
/**
* Set directories that users specifically configured as to NOT import.
* Projects UNDER those directories may be imported, but never project directly
* in one of those directories.
* This does not impact output of {@link #getImportProposals(IProgressMonitor)}
* @param directories
*/
public void setExcludedDirectories(Set<File> directories) {
this.excludedDirectories = directories;
}
/**
* Adds a listener to be notified of progress (detection/configuration of
* sub-projects)
*
* @param listener
*/
public void setListener(RecursiveImportListener listener) {
this.listener = listener;
}
@Override
public IStatus run(IProgressMonitor monitor) {
try {
IWorkspace workspace = ResourcesPlugin.getWorkspace();
IWorkspaceDescription description = workspace.getDescription();
boolean isAutoBuilding = workspace.isAutoBuilding();
if (isAutoBuilding) {
description.setAutoBuilding(false);
workspace.setDescription(description);
}
if (directoriesToImport != null) {
this.deepChildrenDetection = false;
SubMonitor loopMonitor = SubMonitor.convert(monitor,
DataTransferMessages.SmartImportJob_crawling,
directoriesToImport.size() * (configureProjects ? 3 : 2) + 1);
Comparator<File> rootToLeafComparator = new Comparator<File>() {
@Override
public int compare(File arg0, File arg1) {
int lengthDiff = arg0.getAbsolutePath().length() - arg1.getAbsolutePath().length();
if (lengthDiff != 0) {
return lengthDiff;
}
return arg0.compareTo(arg1);
}
};
SortedSet<File> directories = new TreeSet<>(rootToLeafComparator);
directories.addAll(this.directoriesToImport);
SortedMap<File, IProject> leafToRootProjects = new TreeMap<>(Collections.reverseOrder(rootToLeafComparator));
final Set<IProject> alreadyConfiguredProjects = new HashSet<>();
loopMonitor.worked(1);
for (final File directoryToImport : directories) {
final boolean alreadyAnEclipseProject = new File(directoryToImport, IProjectDescription.DESCRIPTION_FILE_NAME).isFile();
try {
IProject newProject = toExistingOrNewProject(directoryToImport, loopMonitor.split(1),
IResource.BACKGROUND_REFRESH);
if (alreadyAnEclipseProject) {
alreadyConfiguredProjects.add(newProject);
}
leafToRootProjects.put(directoryToImport, newProject);
loopMonitor.worked(1);
} catch (CouldNotImportProjectException ex) {
Path path = new Path(directoryToImport.getAbsolutePath());
if (listener != null) {
listener.errorHappened(path, ex);
}
this.errors.put(path, ex);
}
}
if (configureProjects) {
JobGroup multiDirectoriesJobGroup = new JobGroup(
DataTransferMessages.SmartImportJob_configuringSelectedDirectories, 20, 1);
for (final IProject newProject : leafToRootProjects.values()) {
Job directoryJob = new Job(
NLS.bind(DataTransferMessages.SmartImportJob_configuring, newProject.getName())) {
@Override
protected IStatus run(IProgressMonitor aMonitor) {
try {
importProjectAndChildrenRecursively(newProject,
!alreadyConfiguredProjects.contains(newProject), monitor);
return Status.OK_STATUS;
} catch (Exception ex) {
return new Status(IStatus.ERROR, IDEWorkbenchPlugin.IDE_WORKBENCH, ex.getMessage(),
ex);
}
}
};
// Job1 on path1 and Job2 on path2 can be run in parallel IFF path1 isn't a prefix of path2 and vice-versa
directoryJob.setRule(new SubdirectoryOrSameNameSchedulingRule(newProject));
directoryJob.setUser(true);
directoryJob.setJobGroup(multiDirectoriesJobGroup);
directoryJob.schedule();
}
multiDirectoriesJobGroup.join(0, loopMonitor.split(leafToRootProjects.size()));
}
} else { // no specific projects included, consider only root
SubMonitor subMonitor = SubMonitor.convert(monitor, 3);
File rootProjectFile = new File(this.rootDirectory, IProjectDescription.DESCRIPTION_FILE_NAME);
boolean isRootANewProject = !rootProjectFile.isFile();
this.rootProject = toExistingOrNewProject(this.rootDirectory, subMonitor, IResource.NONE);
if (this.configureProjects) {
importProjectAndChildrenRecursively(this.rootProject, isRootANewProject, subMonitor);
if (isRootANewProject && rootProjectWorthBeingRemoved()) {
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
discardRootProject = MessageDialog.openQuestion(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(),
DataTransferMessages.SmartImportJob_discardRootProject_title,
DataTransferMessages.SmartImportJob_discardRootProject_description);
}
});
if (this.discardRootProject) {
this.rootProject.delete(false, true, subMonitor);
if (isRootANewProject) {
rootProjectFile.delete();
}
this.report.remove(this.rootProject);
}
}
}
}
if (isAutoBuilding) {
description.setAutoBuilding(true);
workspace.setDescription(description);
}
} catch (Exception ex) {
return new Status(IStatus.ERROR, IDEWorkbenchPlugin.IDE_WORKBENCH, ex.getMessage(), ex);
}
return Status.OK_STATUS;
}
protected boolean rootProjectWorthBeingRemoved() {
if (this.report.size() == 1) {
return false;
}
List<ProjectConfigurator> rootProjectConfigurators = this.report.get(this.rootProject);
if (rootProjectConfigurators.isEmpty()) {
return true;
}
boolean areOnlyDummyConfigurators = true;
for (ProjectConfigurator configurator : rootProjectConfigurators) {
// TODO: semantics whether configurator is "strong enough" for a root project should be put inside configurator
areOnlyDummyConfigurators &= (configurator instanceof EclipseProjectConfigurator || configurator instanceof EclipseWorkspaceConfigurator);
}
return areOnlyDummyConfigurators;
}
private final class CrawlFolderJob extends Job {
private final IFolder childFolder;
private final Set<IProject> res;
private CrawlFolderJob(String name, IFolder childFolder, Set<IProject> res) {
super(name);
this.childFolder = childFolder;
this.res = res;
}
@Override
public IStatus run(IProgressMonitor progressMonitor) {
SubMonitor subMonitor = null;
if (progressMonitor instanceof SubMonitor) {
subMonitor = (SubMonitor) progressMonitor;
} else {
subMonitor = SubMonitor.convert(progressMonitor);
}
try {
Set<IProject> projectFromCurrentContainer = importProjectAndChildrenRecursively(childFolder, false,
subMonitor);
res.addAll(projectFromCurrentContainer);
return Status.OK_STATUS;
} catch (Exception ex) {
return new Status(IStatus.ERROR, IDEWorkbenchPlugin.IDE_WORKBENCH, ex.getMessage(), ex);
}
}
}
private Set<IProject> searchAndImportChildrenProjectsRecursively(IContainer parentContainer, Set<IPath> directoriesToExclude, final IProgressMonitor progressMonitor) throws Exception {
SubMonitor subMonitor = SubMonitor.convert(progressMonitor, parentContainer.members().length);
for (IProject processedProjects : Collections.synchronizedSet(this.report.keySet())) {
if (processedProjects.getLocation().equals(parentContainer.getLocation())) {
return Collections.emptySet();
}
}
parentContainer.refreshLocal(IResource.DEPTH_ONE, progressMonitor); // make sure we know all children
Set<IFolder> childrenToProcess = new HashSet<>();
final Set<IProject> res = Collections.synchronizedSet(new HashSet<IProject>());
for (IResource childResource : parentContainer.members()) {
if (childResource.getType() == IResource.FOLDER && !childResource.isDerived()) {
boolean excluded = false;
if (directoriesToExclude != null) {
for (IPath excludedPath : directoriesToExclude) {
if (!excludedPath.isPrefixOf(parentContainer.getLocation()) && excludedPath.isPrefixOf(childResource.getLocation())) {
excluded = true;
}
}
}
if (!excluded) {
childrenToProcess.add((IFolder)childResource);
}
}
}
Set<CrawlFolderJob> jobs = new HashSet<>();
for (final IFolder childFolder : childrenToProcess) {
CrawlFolderJob crawlerJob = new CrawlFolderJob(
NLS.bind(DataTransferMessages.SmartImportJob_crawling, childFolder.getLocation().toString()),
childFolder, res);
if (crawlerJobGroup.getMaxThreads() == 0 || crawlerJobGroup.getActiveJobs().size() < crawlerJobGroup.getMaxThreads()) {
crawlerJob.setJobGroup(crawlerJobGroup);
jobs.add(crawlerJob);
crawlerJob.schedule();
} else {
crawlerJob.run(subMonitor);
subMonitor.worked(1);
}
}
for (CrawlFolderJob job : jobs) {
job.join(0, subMonitor.split(1));
}
subMonitor.done();
return res;
}
private Set<IProject> importProjectAndChildrenRecursively(IContainer container, boolean forceFullProjectCheck,
IProgressMonitor progressMonitor) throws Exception {
int allWork = 30 + ProjectConfiguratorExtensionManager.getAllExtensionLabels().size() * 5;
SubMonitor subMonitor = SubMonitor.convert(progressMonitor,
NLS.bind(DataTransferMessages.SmartImportJob_inspecting,
container.getLocation().toFile().getAbsolutePath()),
allWork);
Set<IProject> projectFromCurrentContainer = new HashSet<>();
boolean isAlreadyAnEclipseProject = false;
Set<ProjectConfigurator> mainProjectConfigurators = new HashSet<>();
Set<IPath> excludedPaths = new HashSet<>();
if (this.excludedDirectories != null) {
for (File excludedDirectory : this.excludedDirectories) {
excludedPaths.add(new Path(excludedDirectory.getAbsolutePath()));
}
}
container.refreshLocal(IResource.DEPTH_INFINITE, progressMonitor);
if (!forceFullProjectCheck) {
EclipseProjectConfigurator eclipseProjectConfigurator = new EclipseProjectConfigurator();
if (eclipseProjectConfigurator.shouldBeAnEclipseProject(container, subMonitor.split(1))) {
isAlreadyAnEclipseProject = true;
}
}
if (this.configurationManager == null) {
this.configurationManager = new ProjectConfiguratorExtensionManager();
}
Collection<ProjectConfigurator> activeConfigurators = this.configurationManager.getAllActiveProjectConfigurators(container);
Set<ProjectConfigurator> potentialSecondaryConfigurators = new HashSet<>();
IProject project = null;
for (ProjectConfigurator configurator : activeConfigurators) {
// exclude Eclipse project configurator for root project if is new
if (configurator instanceof EclipseProjectConfigurator && forceFullProjectCheck) {
continue;
}
if (configurator.shouldBeAnEclipseProject(container, subMonitor.split(1))) {
mainProjectConfigurators.add(configurator);
if (project == null) {
// Create project
try {
project = toExistingOrNewProject(container.getLocation().toFile(), subMonitor.split(1),
IResource.BACKGROUND_REFRESH);
} catch (CouldNotImportProjectException ex) {
this.errors.put(container.getLocation(), ex);
if (this.listener != null) {
this.listener.errorHappened(container.getLocation(), ex);
}
return projectFromCurrentContainer;
}
projectFromCurrentContainer.add(project);
}
} else {
potentialSecondaryConfigurators.add(configurator);
}
}
if (!mainProjectConfigurators.isEmpty()) {
project.refreshLocal(IResource.DEPTH_INFINITE, subMonitor.split(1));
}
for (ProjectConfigurator configurator : mainProjectConfigurators) {
IProgressMonitor childMonitor = subMonitor.split(1);
if (configurator instanceof EclipseProjectConfigurator || !isAlreadyAnEclipseProject || this.reconfigureEclipseProjects) {
configurator.configure(project, excludedPaths, childMonitor);
this.report.get(project).add(configurator);
if (this.listener != null) {
listener.projectConfigured(project, configurator);
}
}
excludedPaths.addAll(toPathSet(configurator.getFoldersToIgnore(project, subMonitor.split(20))));
}
Set<IProject> allNestedProjects = new HashSet<>();
if (deepChildrenDetection) {
allNestedProjects.addAll( searchAndImportChildrenProjectsRecursively(container, excludedPaths, progressMonitor) );
excludedPaths.addAll(toPathSet(allNestedProjects));
projectFromCurrentContainer.addAll(allNestedProjects);
}
if (mainProjectConfigurators.isEmpty() && (!isAlreadyAnEclipseProject || forceFullProjectCheck)) {
// Apply secondary configurators
if (project == null) {
// Create project
try {
project = toExistingOrNewProject(container.getLocation().toFile(), subMonitor.split(1),
IResource.BACKGROUND_REFRESH);
} catch (CouldNotImportProjectException ex) {
this.errors.put(container.getLocation(), ex);
if (this.listener != null) {
this.listener.errorHappened(container.getLocation(), ex);
}
return projectFromCurrentContainer;
}
projectFromCurrentContainer.add(project);
}
project.refreshLocal(IResource.DEPTH_ONE, subMonitor.split(1));
// At least depth one, maybe INFINITE is necessary
progressMonitor.setTaskName(
NLS.bind(DataTransferMessages.SmartImportJob_continuingConfiguration, project.getName()));
for (ProjectConfigurator additionalConfigurator : potentialSecondaryConfigurators) {
if (additionalConfigurator.canConfigure(project, excludedPaths, subMonitor.split(1))) {
additionalConfigurator.configure(project, excludedPaths, subMonitor.split(1));
this.report.get(project).add(additionalConfigurator);
if (this.listener != null) {
listener.projectConfigured(project, additionalConfigurator);
}
excludedPaths
.addAll(toPathSet(additionalConfigurator.getFoldersToIgnore(project, subMonitor.split(1))));
}
}
}
subMonitor.done();
return projectFromCurrentContainer;
}
private Set<IPath> toPathSet(Set<? extends IContainer> resources) {
if (resources == null || resources.isEmpty()) {
return Collections.emptySet();
}
Set<IPath> res = new HashSet<>();
for (IContainer container : resources) {
res.add(container.getLocation());
}
return res;
}
/**
* @param directory
* @param workingSets
* @param refreshMode One {@link IResource#BACKGROUND_REFRESH} for background refresh, or {@link IResource#NONE} for immediate refresh
* @return
* @throws Exception
*/
private IProject toExistingOrNewProject(File directory, IProgressMonitor progressMonitor, int refreshMode) throws CouldNotImportProjectException {
try {
SubMonitor subMonitor = SubMonitor.convert(progressMonitor, NLS.bind(
DataTransferMessages.SmartImportJob_importingProjectIntoWorkspace, directory.getAbsolutePath()), 2);
IProject project = projectAlreadyExistsInWorkspace(directory);
if (project == null) {
project = createOrImportProject(directory, subMonitor.split(1));
}
subMonitor.setWorkRemaining(1);
project.open(refreshMode, subMonitor.split(1));
if (!this.report.containsKey(project)) {
this.report.put(project, new ArrayList<ProjectConfigurator>());
}
if (this.listener != null) {
this.listener.projectCreated(project);
}
return project;
} catch (Exception ex) {
throw new CouldNotImportProjectException(directory, ex);
}
}
private IProject projectAlreadyExistsInWorkspace(File directory) {
for (IProject project : workspaceRoot.getProjects()) {
if (project.getLocation().toFile().getAbsoluteFile().equals(directory.getAbsoluteFile())) {
return project;
}
}
return null;
}
private IProject createOrImportProject(File directory, IProgressMonitor progressMonitor) throws Exception {
IProjectDescription desc = null;
File expectedProjectDescriptionFile = new File(directory, IProjectDescription.DESCRIPTION_FILE_NAME);
if (expectedProjectDescriptionFile.exists()) {
desc = ResourcesPlugin.getWorkspace().loadProjectDescription(new Path(expectedProjectDescriptionFile.getAbsolutePath()));
String expectedName = desc.getName();
IProject projectWithSameName = this.workspaceRoot.getProject(expectedName);
if (projectWithSameName.exists()) {
if (projectWithSameName.getLocation().toFile().equals(directory)) {
// project seems already there
return projectWithSameName;
}
throw new CouldNotImportProjectException(directory,
NLS.bind(DataTransferMessages.SmartImportProposals_anotherProjectWithSameNameExists_description, expectedName));
}
} else {
String projectName = directory.getName();
if (this.workspaceRoot.getProject(directory.getName()).exists()) {
int i = 1;
do {
projectName = directory.getName() + '(' + i + ')';
i++;
} while (this.workspaceRoot.getProject(projectName).exists());
}
desc = ResourcesPlugin.getWorkspace().newProjectDescription(projectName);
}
desc.setLocation(new Path(directory.getAbsolutePath()));
IProject res = workspaceRoot.getProject(desc.getName());
res.create(desc, progressMonitor);
PlatformUI.getWorkbench().getWorkingSetManager().addToWorkingSets(res, this.workingSets);
return res;
}
/**
*
* @return the project found/created for the root folder
*/
public IProject getRootProject() {
return this.rootProject;
}
/**
*
* @return The list of projects found/imported and the strategy that were
* used in order to configure them.
*/
public Map<IProject, List<ProjectConfigurator>> getConfiguredProjects() {
return this.report;
}
/**
* @return the import errors that happened.
*/
public Map<IPath, Exception> getErrors() {
return this.errors;
}
/**
*
* @param monitor
* @return the proposals for the import operation.
*/
public Map<File, List<ProjectConfigurator>> getImportProposals(IProgressMonitor monitor) {
if (!this.deepChildrenDetection) {
Map<File, List<ProjectConfigurator>> res = new HashMap<>();
res.put(rootDirectory, Collections.emptyList());
return res;
}
if (this.importProposals == null) {
Map<File, List<ProjectConfigurator>> res = new HashMap<>();
if (this.configurationManager == null) {
this.configurationManager = new ProjectConfiguratorExtensionManager();
}
List<ProjectConfigurator> activeConfigurators = configurationManager
.getAllActiveProjectConfigurators(this.rootDirectory);
SubMonitor loopMonitor = SubMonitor.convert(monitor, activeConfigurators.size());
for (ProjectConfigurator configurator : activeConfigurators) {
Set<File> supportedDirectories = configurator.findConfigurableLocations(
SmartImportJob.this.rootDirectory,
loopMonitor.split(1));
if (supportedDirectories != null) {
for (File supportedDirectory : supportedDirectories) {
if (supportedDirectory.isDirectory()) {
if (!res.containsKey(supportedDirectory)) {
res.put(supportedDirectory, new ArrayList<ProjectConfigurator>());
}
res.get(supportedDirectory).add(configurator);
} else {
IDEWorkbenchPlugin.log("Project detection must return only directories.\n" //$NON-NLS-1$
+ supportedDirectory + " is not a directory.\nContributed by " //$NON-NLS-1$
+ configurator.getClass().getName());
}
}
}
}
for (ProjectConfigurator configurator : activeConfigurators) {
configurator.removeDirtyDirectories(res);
}
this.importProposals = res;
}
return this.importProposals;
}
/**
* @return whether the job is set to configure projects (set natures and
* other).
*/
public boolean isConfigureProjects() {
return this.configureProjects;
}
/**
*
* @return whether the job will look for nested projects in case no
* directory is passed to {@link #setDirectoriesToImport(Set)}
*/
public boolean isDetectNestedProjects() {
return this.deepChildrenDetection;
}
/**
* Sets whether the job should look for nested projects. This value is
* ignored if consumer specifies directories to import via
* {@link #setDirectoriesToImport(Set)}.
*
* @param detectNestedProjects
*/
public void setDetectNestedProjects(boolean detectNestedProjects) {
this.deepChildrenDetection = detectNestedProjects;
}
/**
* Forget the initial import proposals.
*/
public void resetProposals() {
this.importProposals = null;
}
/**
*
* @return The directories that will be crawled for import
*/
public Set<File> getDirectoriesToImport() {
return this.directoriesToImport;
}
@Override
public boolean belongsTo(Object family) {
return family == SmartImportJob.class;
}
}