/*
* 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.Map;
import java.util.Set;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;
import com.google.common.collect.Maps;
import com.gradleware.tooling.toolingmodel.OmniEclipseProject;
import com.gradleware.tooling.toolingmodel.OmniEclipseProjectDependency;
import com.gradleware.tooling.toolingmodel.OmniExternalDependency;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.jdt.core.IClasspathContainer;
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.preferences.PersistentModel;
import org.eclipse.buildship.core.util.classpath.ClasspathUtils;
import org.eclipse.buildship.core.workspace.GradleClasspathContainer;
/**
* Updates the classpath container of the target project.
* <p/>
* The update is triggered via {@link #updateFromModel(IJavaProject, OmniEclipseProject, Set, IProgressMonitor)}.
* The method executes synchronously and unprotected, without thread synchronization or job scheduling.
* <p/>
* The update logic composes a new classpath container containing all project and external
* dependencies defined in the Gradle model. At the end of the execution the old classpath
* container is replaced by the one being created.
* <p/>
* If an invalid external dependency is received (anything else, than a folder, {@code .jar} file
* or {@code .zip} file) the given entry is omitted from the classpath container. Due to
* performance reasons only the file extension is checked.
*/
final class GradleClasspathContainerUpdater {
private final IJavaProject eclipseProject;
private final OmniEclipseProject gradleProject;
private final Map<File, OmniEclipseProject> projectDirToProject;
private GradleClasspathContainerUpdater(IJavaProject eclipseProject, OmniEclipseProject gradleProject, Set<OmniEclipseProject> allGradleProjects) {
this.eclipseProject = Preconditions.checkNotNull(eclipseProject);
this.gradleProject = Preconditions.checkNotNull(gradleProject);
this.projectDirToProject = Maps.newHashMap();
for (OmniEclipseProject project : gradleProject.getRoot().getAll()) {
this.projectDirToProject.put(project.getProjectDirectory(), project);
}
}
private void updateClasspathContainer(PersistentModelBuilder persistentModel, IProgressMonitor monitor) throws JavaModelException {
ImmutableList<IClasspathEntry> containerEntries = collectClasspathContainerEntries();
setClasspathContainer(this.eclipseProject, containerEntries, monitor);
persistentModel.classpath(containerEntries);
}
private ImmutableList<IClasspathEntry> collectClasspathContainerEntries() {
List<IClasspathEntry> externalDependencies = collectExternalDependencies();
List<IClasspathEntry> projectDependencies = collectProjectDependencies();
boolean hasExportedEntry = FluentIterable.from(externalDependencies).anyMatch(new Predicate<IClasspathEntry>() {
@Override
public boolean apply(IClasspathEntry entry) {
return entry.isExported();
}
});
// Gradle distributions <2.5 rely on exports to define the project classpath. Unfortunately
// that logic is broken if dependency excludes are defined in the build scripts. To work
// around that, external dependencies must be defined before project dependencies. For more
// details, visit Bug 473348.
if (hasExportedEntry) {
return ImmutableList.<IClasspathEntry>builder().addAll(externalDependencies).addAll(projectDependencies).build();
} else {
return ImmutableList.<IClasspathEntry>builder().addAll(projectDependencies).addAll(externalDependencies).build();
}
}
private List<IClasspathEntry> collectExternalDependencies() {
Builder<IClasspathEntry> result = ImmutableList.builder();
for (OmniExternalDependency dependency : this.gradleProject.getExternalDependencies()) {
File dependencyFile = dependency.getFile();
boolean linkedResourceCreated = tryCreatingLinkedResource(dependencyFile, result);
if (!linkedResourceCreated) {
String dependencyName = dependencyFile.getName();
// Eclipse only accepts folders and archives as external dependencies (but not, for example, a DLL)
if (dependencyFile.isDirectory() || dependencyName.endsWith(".jar") || dependencyName.endsWith(".zip")) {
IPath path = org.eclipse.core.runtime.Path.fromOSString(dependencyFile.getAbsolutePath());
File dependencySource = dependency.getSource();
IPath sourcePath = dependencySource != null ? org.eclipse.core.runtime.Path.fromOSString(dependencySource.getAbsolutePath()) : null;
IClasspathEntry entry = JavaCore.newLibraryEntry(path, sourcePath, null, ClasspathUtils.createAccessRules(dependency), ClasspathUtils
.createClasspathAttributes(dependency), dependency.isExported());
result.add(entry);
}
}
}
return result.build();
}
private boolean tryCreatingLinkedResource(File dependencyFile, Builder<IClasspathEntry> result) {
if (!dependencyFile.exists()) {
IPath path = new Path("/" + dependencyFile.getPath());
IResource member = this.eclipseProject.getProject().findMember(path);
if (member != null) {
IClasspathEntry entry = JavaCore.newLibraryEntry(member.getFullPath(), null, null);
result.add(entry);
return true;
}
}
return false;
}
private List<IClasspathEntry> collectProjectDependencies() {
Builder<IClasspathEntry> result = ImmutableList.builder();
for (OmniEclipseProjectDependency dependency : this.gradleProject.getProjectDependencies()) {
IPath path = new Path("/" + dependency.getPath());
IClasspathEntry entry = JavaCore.newProjectEntry(path, ClasspathUtils.createAccessRules(dependency), true, ClasspathUtils.createClasspathAttributes(dependency), dependency.isExported());
result.add(entry);
}
return result.build();
}
/**
* Updates the classpath container of the target project based on the given Gradle model.
* The container will be persisted so it does not have to be reloaded after the workbench is restarted.
*/
public static void updateFromModel(IJavaProject eclipseProject, OmniEclipseProject gradleProject, Set<OmniEclipseProject> allGradleProjects, PersistentModelBuilder persistentModel, IProgressMonitor monitor) throws JavaModelException {
GradleClasspathContainerUpdater updater = new GradleClasspathContainerUpdater(eclipseProject, gradleProject, allGradleProjects);
updater.updateClasspathContainer(persistentModel, monitor);
}
/**
* Updates the classpath container from the state stored by the last call to {@link #updateFromModel(IJavaProject, OmniEclipseProject, IProgressMonitor)}.
*/
public static boolean updateFromStorage(IJavaProject eclipseProject, IProgressMonitor monitor) throws JavaModelException {
PersistentModel model = CorePlugin.modelPersistence().loadModel(eclipseProject.getProject());
if (model.isPresent()) {
setClasspathContainer(eclipseProject, model.getClasspath(), monitor);
return true;
} else {
return false;
}
}
/**
* Resolves the classpath container to an empty list.
*/
public static void clear(IJavaProject eclipseProject, IProgressMonitor monitor) throws JavaModelException {
setClasspathContainer(eclipseProject, ImmutableList.<IClasspathEntry>of(), monitor);
}
private static void setClasspathContainer(IJavaProject eclipseProject, List<IClasspathEntry> classpathEntries, IProgressMonitor monitor) throws JavaModelException {
IClasspathContainer classpathContainer = GradleClasspathContainer.newInstance(classpathEntries);
JavaCore.setClasspathContainer(GradleClasspathContainer.CONTAINER_PATH, new IJavaProject[]{eclipseProject}, new IClasspathContainer[]{classpathContainer}, monitor);
}
}