/*
* Copyright (c) 2015 the original author or authors.
* 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:
* Etienne Studer & Donát Csikós (Gradle Inc.) - initial API and implementation and initial documentation
* Simon Scholz <simon.scholz@vogella.com> - Bug 473348
*/
package org.eclipse.buildship.core.workspace.internal;
import java.io.File;
import java.util.List;
import java.util.Set;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.gradleware.tooling.toolingmodel.OmniEclipseProject;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.buildship.core.CorePlugin;
import org.eclipse.buildship.core.configuration.BuildConfiguration;
import org.eclipse.buildship.core.configuration.ConfigurationManager;
import org.eclipse.buildship.core.configuration.GradleProjectNature;
import org.eclipse.buildship.core.configuration.ProjectConfiguration;
import org.eclipse.buildship.core.workspace.NewProjectHandler;
/**
* Synchronizes the given Gradle build with the Eclipse workspace. The algorithm is as follows:
* <p/>
* <ol>
* <li>Uncouple all open workspace projects for which there is no corresponding Gradle project in the Gradle build anymore
* <ol>
* <li>the Gradle nature is removed</li>
* <li>the derived resource markers are removed</li>
* <li>the Gradle settings file is removed</li>
* </ol>
* </li>
* <li>Synchronize all Gradle projects of the Gradle build with the Eclipse workspace project counterparts:
* <ul>
* <li>
* If there is a project in the workspace at the location of the Gradle project, the synchronization is as follows:
* <ol>
* <li>If the workspace project is closed, the project is left unchanged</li>
* <li>If the workspace project is open:
* <ul>
* <li>the project name is updated</li>
* <li>the Gradle settings file is written</li>
* <li>the linked resources are set</li>
* <li>the derived resources are marked</li>
* <li>the project natures and build commands are set</li>
* <li>if the Gradle project is a Java project
* <ul>
* <li>the Java nature is added </li>
* <li>the source compatibility settings are updated</li>
* <li>the set of source folders is updated</li>
* <li>the Gradle classpath container is updated</li>
* </ul>
* </li>
* </ul>
* </li>
* </ol>
* </li>
* <li>
* If there is an Eclipse project at the location of the Gradle project, i.e. there is a .project file in that folder, then
* the {@link NewProjectHandler} decides whether to import it and whether to keep or overwrite that existing .project file.
* The imported project is then synchronized as specified above.
* </li>
* <li>If there is no project in the workspace, nor an Eclipse project at the location of the Gradle build, then
* the {@link NewProjectHandler} decides whether to import it.
* The imported project is then synchronized as specified above.
* </li>
* </ul>
* </li>
* </ol>
*
* <p/>
* This operation changes resources. It will acquire the workspace scheduling rule to ensure an atomic operation.
*
*/
final class SynchronizeGradleBuildOperation implements IWorkspaceRunnable {
private final Set<OmniEclipseProject> allProjects;
private final BuildConfiguration buildConfig;
private final NewProjectHandler newProjectHandler;
SynchronizeGradleBuildOperation(Set<OmniEclipseProject> allProjects, BuildConfiguration buildConfig, NewProjectHandler newProjectHandler) {
this.allProjects = allProjects;
this.buildConfig = buildConfig;
this.newProjectHandler = newProjectHandler;
}
@Override
public void run(IProgressMonitor monitor) throws CoreException {
SubMonitor progress = SubMonitor.convert(monitor);
progress.setTaskName(String.format("Synchronizing Gradle build at %s", this.buildConfig.getRootProjectDirectory()));
synchronizeProjectsWithWorkspace(progress);
}
private void synchronizeProjectsWithWorkspace(SubMonitor progress) throws CoreException {
// collect Gradle projects and Eclipse workspace projects to sync
List<IProject> decoupledWorkspaceProjects = getOpenWorkspaceProjectsRemovedFromGradleBuild();
progress.setWorkRemaining(decoupledWorkspaceProjects.size() + this.allProjects.size());
// uncouple the open workspace projects that do not have a corresponding Gradle project anymore
for (IProject project : decoupledWorkspaceProjects) {
uncoupleWorkspaceProjectFromGradle(project, progress.newChild(1));
}
// synchronize the Gradle projects with their corresponding workspace projects
for (final OmniEclipseProject gradleProject : this.allProjects) {
ResourcesPlugin.getWorkspace().run(new IWorkspaceRunnable() {
@Override
public void run(IProgressMonitor monitor) throws CoreException {
synchronizeGradleProjectWithWorkspaceProject(gradleProject, SubMonitor.convert(monitor));
}
}, progress.newChild(1));
}
}
private List<IProject> getOpenWorkspaceProjectsRemovedFromGradleBuild() {
// in the workspace, find all projects with a Gradle nature that belong to the same Gradle build (based on the root project directory) but
// which do not match the location of one of the Gradle projects of that build
final Set<File> gradleProjectDirectories = FluentIterable.from(this.allProjects).transform(new Function<OmniEclipseProject, File>() {
@Override
public File apply(OmniEclipseProject gradleProject) {
return gradleProject.getProjectDirectory();
}
}).toSet();
ImmutableList<IProject> allWorkspaceProjects = CorePlugin.workspaceOperations().getAllProjects();
return FluentIterable.from(allWorkspaceProjects).filter(GradleProjectNature.isPresentOn()).filter(new Predicate<IProject>() {
@Override
public boolean apply(IProject project) {
BuildConfiguration buildConfiguration = CorePlugin.configurationManager().loadProjectConfiguration(project).getBuildConfiguration();
return buildConfiguration.getRootProjectDirectory().equals(SynchronizeGradleBuildOperation.this.buildConfig.getRootProjectDirectory())
&& (project.getLocation() == null || !gradleProjectDirectories.contains(project.getLocation().toFile()));
}
}).toList();
}
private void synchronizeGradleProjectWithWorkspaceProject(OmniEclipseProject project, SubMonitor progress) throws CoreException {
progress.setWorkRemaining(1);
// save the project configuration
ConfigurationManager configManager = CorePlugin.configurationManager();
ProjectConfiguration projectConfig = configManager.createProjectConfiguration(this.buildConfig, project.getProjectDirectory());
configManager.saveProjectConfiguration(projectConfig);
progress.subTask(String.format("Synchronize Gradle project %s with workspace project", project.getName()));
// check if a project already exists in the workspace at the location of the Gradle project to import
Optional<IProject> workspaceProject = CorePlugin.workspaceOperations().findProjectByLocation(project.getProjectDirectory());
SubMonitor childProgress = progress.newChild(1, SubMonitor.SUPPRESS_ALL_LABELS);
if (workspaceProject.isPresent()) {
synchronizeWorkspaceProject(project, workspaceProject.get(), childProgress);
} else {
if (project.getProjectDirectory().exists() && this.newProjectHandler.shouldImport(project)) {
synchronizeNonWorkspaceProject(project, childProgress);
}
}
}
private void synchronizeWorkspaceProject(OmniEclipseProject project, IProject workspaceProject, SubMonitor progress) throws CoreException {
if (workspaceProject.isAccessible()) {
synchronizeOpenWorkspaceProject(project, workspaceProject, progress);
} else {
synchronizeClosedWorkspaceProject(progress);
}
}
private void synchronizeOpenWorkspaceProject(OmniEclipseProject project, IProject workspaceProject, SubMonitor progress) throws CoreException {
progress.setWorkRemaining(9);
//currently lots of our synchronization logic assumes that the whole resource tree is readable.
CorePlugin.workspaceOperations().refreshProject(workspaceProject, progress.newChild(1));
workspaceProject = ProjectNameUpdater.updateProjectName(workspaceProject, project, this.allProjects, progress.newChild(1));
CorePlugin.workspaceOperations().addNature(workspaceProject, GradleProjectNature.ID, progress.newChild(1));
PersistentModelBuilder persistentModel = new PersistentModelBuilder(CorePlugin.modelPersistence().loadModel(workspaceProject));
LinkedResourcesUpdater.update(workspaceProject, project.getLinkedResources(), persistentModel, progress.newChild(1));
GradleFolderUpdater.update(workspaceProject, project, persistentModel, progress.newChild(1));
ProjectNatureUpdater.update(workspaceProject, project.getProjectNatures(), progress.newChild(1));
BuildCommandUpdater.update(workspaceProject, project.getBuildCommands(), progress.newChild(1));
if (isJavaProject(project)) {
synchronizeJavaProject(project, workspaceProject, persistentModel, progress);
} else {
persistentModel.classpath(ImmutableList.<IClasspathEntry>of());
}
CorePlugin.modelPersistence().saveModel(persistentModel.build());
}
private void synchronizeJavaProject(final OmniEclipseProject project, final IProject workspaceProject, final PersistentModelBuilder persistentModel, SubMonitor progress) throws CoreException {
JavaCore.run(new IWorkspaceRunnable() {
@Override
public void run(IProgressMonitor monitor) throws CoreException {
SubMonitor progress = SubMonitor.convert(monitor);
synchronizeJavaProjectInTransaction(project, workspaceProject, persistentModel, progress);
}
}, progress.newChild(1));
}
private void synchronizeJavaProjectInTransaction(final OmniEclipseProject project, final IProject workspaceProject, PersistentModelBuilder persistentModel, SubMonitor progress) throws JavaModelException, CoreException {
progress.setWorkRemaining(8);
//old Gradle versions did not expose natures, so we need to add the Java nature explicitly
CorePlugin.workspaceOperations().addNature(workspaceProject, JavaCore.NATURE_ID, progress.newChild(1));
IJavaProject javaProject = JavaCore.create(workspaceProject);
OutputLocationUpdater.update(javaProject, project.getOutputLocation(), progress.newChild(1));
SourceFolderUpdater.update(javaProject, project.getSourceDirectories(), progress.newChild(1));
LibraryFilter.update(javaProject, project, progress.newChild(1));
ClasspathContainerUpdater.update(javaProject, project.getClasspathContainers(), project.getJavaSourceSettings().get(), progress.newChild(1));
JavaSourceSettingsUpdater.update(javaProject, project, progress.newChild(1));
GradleClasspathContainerUpdater.updateFromModel(javaProject, project, SynchronizeGradleBuildOperation.this.allProjects, persistentModel, progress.newChild(1));
WtpClasspathUpdater.update(javaProject, project, progress.newChild(1));
}
private boolean isJavaProject(OmniEclipseProject project) {
return project.getJavaSourceSettings().isPresent();
}
private void synchronizeClosedWorkspaceProject(SubMonitor childProgress) {
// do not modify closed projects
}
private void synchronizeNonWorkspaceProject(OmniEclipseProject project, SubMonitor progress) throws CoreException {
progress.setWorkRemaining(2);
IProject workspaceProject;
// check if an Eclipse project already exists at the location of the Gradle project to import
Optional<IProjectDescription> projectDescription = CorePlugin.workspaceOperations().findProjectDescriptor(project.getProjectDirectory(), progress.newChild(1));
if (projectDescription.isPresent()) {
workspaceProject = addExistingEclipseProjectToWorkspace(project, projectDescription.get(), progress.newChild(1));
} else {
workspaceProject = addNewEclipseProjectToWorkspace(project, progress.newChild(1));
}
this.newProjectHandler.afterImport(workspaceProject, project);
}
private IProject addExistingEclipseProjectToWorkspace(OmniEclipseProject project, IProjectDescription projectDescription, SubMonitor progress) throws CoreException {
progress.setWorkRemaining(3);
ProjectNameUpdater.ensureProjectNameIsFree(project, this.allProjects, progress.newChild(1));
IProject workspaceProject = CorePlugin.workspaceOperations().includeProject(projectDescription, ImmutableList.<String>of(), progress.newChild(1));
synchronizeOpenWorkspaceProject(project, workspaceProject, progress.newChild(1));
return workspaceProject;
}
private IProject addNewEclipseProjectToWorkspace(OmniEclipseProject project, SubMonitor progress) throws CoreException {
progress.setWorkRemaining(3);
ProjectNameUpdater.ensureProjectNameIsFree(project, this.allProjects, progress.newChild(1));
IProject workspaceProject = CorePlugin.workspaceOperations().createProject(project.getName(), project.getProjectDirectory(), ImmutableList.<String>of(), progress.newChild(1));
synchronizeOpenWorkspaceProject(project, workspaceProject, progress.newChild(1));
return workspaceProject;
}
private void uncoupleWorkspaceProjectFromGradle(IProject workspaceProject, SubMonitor monitor) {
monitor.setWorkRemaining(3);
monitor.subTask(String.format("Uncouple workspace project %s from Gradle", workspaceProject.getName()));
CorePlugin.workspaceOperations().refreshProject(workspaceProject, monitor.newChild(1, SubMonitor.SUPPRESS_ALL_LABELS));
CorePlugin.workspaceOperations().removeNature(workspaceProject, GradleProjectNature.ID, monitor.newChild(1, SubMonitor.SUPPRESS_ALL_LABELS));
CorePlugin.modelPersistence().deleteModel(workspaceProject);
CorePlugin.configurationManager().deleteProjectConfiguration(workspaceProject);
}
}