/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.redhat.ceylon.eclipse.core.classpath;
import static com.redhat.ceylon.eclipse.core.builder.CeylonBuilder.getCeylonClassesOutputFolder;
import static com.redhat.ceylon.eclipse.core.builder.CeylonBuilder.isExplodeModulesEnabled;
import static com.redhat.ceylon.eclipse.core.classpath.CeylonClasspathUtil.ceylonSourceArchiveToJavaSourceArchive;
import static com.redhat.ceylon.eclipse.java2ceylon.Java2CeylonProxies.modelJ2C;
import static com.redhat.ceylon.eclipse.java2ceylon.Java2CeylonProxies.utilJ2C;
import static com.redhat.ceylon.eclipse.ui.CeylonPlugin.PLUGIN_ID;
import static com.redhat.ceylon.eclipse.util.InteropUtils.toJavaString;
import static java.util.Arrays.asList;
import static java.util.Collections.synchronizedSet;
import static org.eclipse.core.resources.ResourcesPlugin.getWorkspace;
import static org.eclipse.jdt.core.JavaCore.newLibraryEntry;
import static org.eclipse.jdt.core.JavaCore.setClasspathContainer;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
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.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jdt.core.ElementChangedEvent;
import org.eclipse.jdt.core.IClasspathAttribute;
import org.eclipse.jdt.core.IClasspathContainer;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IElementChangedListener;
import org.eclipse.jdt.core.IJavaElementDelta;
import org.eclipse.jdt.core.IJavaModelMarker;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.internal.core.DeltaProcessingState;
import org.eclipse.jdt.internal.core.JavaElementDelta;
import org.eclipse.jdt.internal.core.JavaModelManager;
import org.eclipse.jdt.internal.ui.packageview.PackageExplorerContentProvider;
import org.eclipse.jdt.internal.ui.util.CoreUtility;
import com.redhat.ceylon.cmr.api.ArtifactContext;
import com.redhat.ceylon.cmr.api.RepositoryManager;
import com.redhat.ceylon.cmr.impl.MavenRepository;
import com.redhat.ceylon.common.ModuleSpec;
import com.redhat.ceylon.compiler.typechecker.TypeChecker;
import com.redhat.ceylon.compiler.typechecker.context.Context;
import com.redhat.ceylon.eclipse.core.builder.CeylonBuilder;
import com.redhat.ceylon.eclipse.core.model.LookupEnvironmentUtilities;
import com.redhat.ceylon.ide.common.model.BaseIdeModule;
import com.redhat.ceylon.ide.common.model.CeylonIdeConfig;
import com.redhat.ceylon.ide.common.model.CeylonProject;
import com.redhat.ceylon.ide.common.model.CeylonProjectConfig;
import com.redhat.ceylon.ide.common.platform.platformUtils_;
import com.redhat.ceylon.ide.common.util.ProgressMonitor;
import com.redhat.ceylon.ide.common.util.ProgressMonitor$impl;
import com.redhat.ceylon.ide.common.util.ProgressMonitorChild;
import com.redhat.ceylon.model.cmr.ArtifactResultType;
import com.redhat.ceylon.model.typechecker.model.Module;
/**
* Eclipse classpath container that will contain the Ceylon resolved entries.
*/
public class CeylonProjectModulesContainer implements IClasspathContainer {
public static final String CONTAINER_ID = PLUGIN_ID + ".cpcontainer.CEYLON_CONTAINER";
private IClasspathEntry[] classpathEntries;
private IPath path;
//private String jdtVersion;
private IJavaProject javaProject;
private Set<String> modulesWithSourcesAlreadySearched = synchronizedSet(new HashSet<String>());
public IJavaProject getJavaProject() {
return javaProject;
}
public IClasspathAttribute[] getAttributes() {
return attributes;
}
/**
* attributes attached to the container but not Ceylon related (Webtools or AspectJfor instance)
*/
private IClasspathAttribute[] attributes = new IClasspathAttribute[0];
public CeylonProjectModulesContainer(IJavaProject javaProject, IPath path,
IClasspathEntry[] classpathEntries, IClasspathAttribute[] attributes) {
this.path = path;
this.attributes = attributes;
this.classpathEntries = classpathEntries;
this.javaProject = javaProject;
}
public CeylonProjectModulesContainer(IProject project) {
javaProject = JavaCore.create(project);
path = new Path(CeylonProjectModulesContainer.CONTAINER_ID + "/default");
classpathEntries = new IClasspathEntry[0];
attributes = new IClasspathAttribute[0];
}
public CeylonProjectModulesContainer(CeylonProjectModulesContainer cp) {
path = cp.path;
javaProject = cp.javaProject;
classpathEntries = cp.classpathEntries;
attributes = cp.attributes;
modulesWithSourcesAlreadySearched = cp.modulesWithSourcesAlreadySearched;
}
public String getDescription() {
return "Ceylon Project Modules";
}
public int getKind() {
return K_APPLICATION;
}
public IPath getPath() {
return path;
}
public IClasspathEntry[] getClasspathEntries() {
return classpathEntries;
}
public IClasspathEntry addNewClasspathEntryIfNecessary(IPath modulePath) {
synchronized (classpathEntries) {
for (IClasspathEntry cpEntry : classpathEntries) {
if (cpEntry.getPath().equals(modulePath)) {
return null;
}
}
IClasspathEntry newEntry = newLibraryEntry(modulePath, null, null);
IClasspathEntry[] newClasspathEntries = new IClasspathEntry[classpathEntries.length + 1];
if (classpathEntries.length > 0) {
System.arraycopy(classpathEntries, 0, newClasspathEntries, 0, classpathEntries.length);
}
newClasspathEntries[classpathEntries.length] = newEntry;
classpathEntries = newClasspathEntries;
return newEntry;
}
}
/*private static final ISchedulingRule RESOLVE_EVENT_RULE = new ISchedulingRule() {
public boolean contains(ISchedulingRule rule) {
return rule == this;
}
public boolean isConflicting(ISchedulingRule rule) {
return rule == this;
}
};*/
public void runReconfigure() {
modulesWithSourcesAlreadySearched.clear();
Job job = new Job("Resolving dependencies for project " +
getJavaProject().getElementName()) {
@Override
protected IStatus run(IProgressMonitor monitor) {
final IProject project = javaProject.getProject();
ProgressMonitor$impl<IProgressMonitor>.Progress progress =
utilJ2C().wrapProgressMonitor(monitor)
.Progress$new$(1000,
ceylon.language.String.instance("Resolving classpath of project " + project.getName()));
try {
final IClasspathEntry[] classpath = constructModifiedClasspath(javaProject);
javaProject.setRawClasspath(classpath, monitor);
boolean changed = resolveClasspath(progress.newChild(800), false);
if(changed) {
refreshClasspathContainer(progress.newChild(200));
}
// Rebuild the project :
// - without referenced projects
// - with referencing projects
// - and force the rebuild even if the model is already typechecked
Job job = new BuildProjectAfterClasspathChangeJob("Rebuild of project " +
project.getName(), project, false, true, true);
job.setRule(project.getWorkspace().getRoot());
job.schedule(3000);
job.setPriority(Job.BUILD);
return Status.OK_STATUS;
}
catch (CoreException e) {
e.printStackTrace();
return new Status(IStatus.ERROR, PLUGIN_ID,
"could not resolve dependencies", e);
}
finally {
progress.destroy(null);
}
}
};
job.setUser(false);
job.setPriority(Job.BUILD);
job.setRule(getWorkspace().getRoot());
job.schedule();
}
private IClasspathEntry[] constructModifiedClasspath(IJavaProject javaProject)
throws JavaModelException {
IClasspathEntry newEntry = JavaCore.newContainerEntry(path, null,
new IClasspathAttribute[0], true);
IClasspathEntry[] entries = javaProject.getRawClasspath();
List<IClasspathEntry> newEntries = new ArrayList<IClasspathEntry>(asList(entries));
int index = 0;
boolean mustReplace = false;
boolean projectModulesEntryWasExported = false;
for (IClasspathEntry entry: newEntries) {
if (entry.getPath().equals(newEntry.getPath()) ) {
mustReplace = true;
projectModulesEntryWasExported = entry.isExported();
break;
}
index++;
}
newEntry = JavaCore.newContainerEntry(path, null,
new IClasspathAttribute[0], !mustReplace || projectModulesEntryWasExported);
if (mustReplace) {
newEntries.set(index, newEntry);
}
else {
newEntries.add(newEntry);
}
return (IClasspathEntry[]) newEntries.toArray(new IClasspathEntry[newEntries.size()]);
}
void notifyUpdateClasspathEntries() {
// Changes to resolved classpath are not announced by JDT Core
// and so PackageExplorer does not properly refresh when we update
// the classpath container.
// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=154071
DeltaProcessingState s = JavaModelManager.getJavaModelManager().deltaState;
synchronized (s) {
IElementChangedListener[] listeners = s.elementChangedListeners;
for (int i = 0; i < listeners.length; i++) {
if (listeners[i] instanceof PackageExplorerContentProvider) {
JavaElementDelta delta = new JavaElementDelta(javaProject);
delta.changed(IJavaElementDelta.F_RESOLVED_CLASSPATH_CHANGED);
listeners[i].elementChanged(new ElementChangedEvent(delta,
ElementChangedEvent.POST_CHANGE));
}
}
}
//I've disabled this because I don't really like having it, but
//it does seem to help with the issue of archives appearing
//empty in the package manager
/*try {
javaProject.getProject().refreshLocal(IResource.DEPTH_ONE, null);
}
catch (CoreException e) {
e.printStackTrace();
}*/
}
/**
* Resolves the classpath entries for this container.
* @param monitor
* @param reparse
* @return true if the classpath was changed, false otherwise.
*/
public boolean resolveClasspath(ProgressMonitor<IProgressMonitor> mon, boolean reparse) {
IJavaProject javaProject = getJavaProject();
IProject project = javaProject.getProject();
ProgressMonitor$impl<IProgressMonitor>.Progress ceylonMonitor = mon.Progress$new$(1000,
ceylon.language.String.instance("Resolving classpath of project " + project.getName()));
CeylonProject<IProject, IResource, IFolder, IFile> ceylonProject = modelJ2C().ceylonModel().getProject(project);
try {
TypeChecker typeChecker = null;
if (!reparse) {
typeChecker = ceylonProject.getTypechecker();
}
IClasspathEntry[] oldEntries = classpathEntries;
if (typeChecker==null) {
IClasspathEntry explodeFolderEntry = null;
if (oldEntries != null) {
for (IClasspathEntry entry : oldEntries) {
if (entry.getPath() != null && entry.getPath().equals(getCeylonClassesOutputFolder(project).getFullPath())) {
explodeFolderEntry = entry;
break;
}
}
}
IClasspathEntry[] resetEntries = explodeFolderEntry == null ?
new IClasspathEntry[] {} :
new IClasspathEntry[] {explodeFolderEntry};
JavaCore.setClasspathContainer(getPath(),
new IJavaProject[]{javaProject},
new IClasspathContainer[]{ new CeylonProjectModulesContainer(javaProject, getPath(), resetEntries, attributes)} , ceylonMonitor.newChild(100).getWrapped());
ceylonProject.parseCeylonModel(ceylonMonitor.newChild(800));
}
typeChecker = ceylonProject.getTypechecker();
IFolder explodedModulesFolder = getCeylonClassesOutputFolder(project);
if (isExplodeModulesEnabled(project)) {
if (!explodedModulesFolder.exists()) {
CoreUtility.createDerivedFolder(explodedModulesFolder, true, true, ceylonMonitor.newChild(10).getWrapped());
} else {
if (!explodedModulesFolder.isDerived()) {
explodedModulesFolder.setDerived(true, ceylonMonitor.newChild(10).getWrapped());
}
}
}
else {
if (explodedModulesFolder.exists()) {
explodedModulesFolder.delete(true, ceylonMonitor.newChild(10).getWrapped());
}
}
final Collection<IClasspathEntry> paths = findModuleArchivePaths(
javaProject, project, typeChecker);
CeylonProjectModulesContainer currentContainer = (CeylonProjectModulesContainer) JavaCore.getClasspathContainer(path, javaProject);
if (oldEntries == null ||
oldEntries != currentContainer.classpathEntries ||
!paths.equals(asList(oldEntries))) {
this.classpathEntries = paths.toArray(new IClasspathEntry[paths.size()]);
return true;
}
}
catch (CoreException e) {
e.printStackTrace();
}
finally {
ceylonMonitor.destroy(null);
}
return false;
}
public void refreshClasspathContainer(ProgressMonitorChild<IProgressMonitor> monitor) throws JavaModelException {
IJavaProject javaProject = getJavaProject();
setClasspathContainer(path, new IJavaProject[] { javaProject },
new IClasspathContainer[] {new CeylonProjectModulesContainer(this)}, monitor.getWrapped());
LookupEnvironmentUtilities.Provider modelLoader = (LookupEnvironmentUtilities.Provider) CeylonBuilder.getProjectModelLoader(javaProject.getProject());
if (modelLoader != null) {
modelLoader.refreshNameEnvironment();
}
//update the package manager UI
new Job("update package manager") {
@Override
protected IStatus run(IProgressMonitor monitor) {
notifyUpdateClasspathEntries();
return Status.OK_STATUS;
}
}.schedule();
}
private Collection<IClasspathEntry> findModuleArchivePaths(
IJavaProject javaProject, IProject project, TypeChecker typeChecker)
throws JavaModelException, CoreException {
final Map<String, IClasspathEntry> paths = new TreeMap<String, IClasspathEntry>();
ModuleSpec jdkProviderSpec = null;
CeylonProjectConfig ceylonConfig = modelJ2C().ceylonConfig(project);
if (ceylonConfig != null) {
ceylon.language.String jdkProviderString = ceylonConfig.getJdkProvider();
if (jdkProviderString != null) {
jdkProviderSpec = ModuleSpec.parse(jdkProviderString.toString());
}
}
Context context = typeChecker.getContext();
RepositoryManager provider = context.getRepositoryManager();
Set<Module> modulesToAdd = context.getModules().getListOfModules();
//modulesToAdd.add(projectModules.getLanguageModule());
for (Module module: modulesToAdd) {
BaseIdeModule jdtModule = (BaseIdeModule) module;
String name = module.getNameAsString();
if (name.equals(Module.DEFAULT_MODULE_NAME) ||
!(jdtModule.getIsJavaBinaryArchive() || jdtModule.getIsCeylonArchive()) ||
module.equals(module.getLanguageModule()) ||
! module.isAvailable()) {
continue;
}
if (jdkProviderSpec != null &&
jdkProviderSpec.getName().equals(module.getNameAsString())) {
continue;
}
IPath modulePath = getModuleArchive(provider, jdtModule);
if (modulePath!=null) {
IPath srcPath = null;
for (IProject p: project.getReferencedProjects()) {
if (p.isAccessible()
&& p.getLocation().isPrefixOf(modulePath)) {
//the module belongs to a referenced
//project, so use the project source
srcPath = p.getLocation();
break;
}
}
if (srcPath==null) {
for (IClasspathEntry entry : classpathEntries) {
if (entry.getPath().equals(modulePath)) {
srcPath = entry.getSourceAttachmentPath();
break;
}
}
}
if (srcPath==null &&
!modulesWithSourcesAlreadySearched.contains(module.toString())) {
//otherwise, use the src archive
srcPath = getSourceArchive(provider, jdtModule);
if ((srcPath == null || srcPath.equals(modulePath))
&& jdtModule.getIsJavaBinaryArchive()) {
CeylonIdeConfig ideConfig = modelJ2C().ideConfig(project);
if (ideConfig != null) {
ceylon.language.String attachment =
ideConfig.getSourceAttachment(
jdtModule.getNameAsString(), jdtModule.getVersion());
if (attachment != null) {
String a = attachment.toString();
srcPath=new Path(a);
if (! srcPath.isAbsolute()) {
if (a.startsWith("../") ||
a.startsWith("./")) {
srcPath = project.getLocation().append(srcPath);
} else {
srcPath = project.getFullPath().append(srcPath);
}
}
}
}
}
}
modulesWithSourcesAlreadySearched.add(module.toString());
IClasspathEntry newEntry = newLibraryEntry(modulePath, srcPath, null);
paths.put(newEntry.toString(), newEntry);
}
else {
// FIXME: ideally we should find the module.java file and put the marker there, but
// I've no idea how to find it and which import is the cause of the import problem
// as it could be transitive
IMarker marker = project.createMarker(IJavaModelMarker.BUILDPATH_PROBLEM_MARKER);
marker.setAttribute(IMarker.MESSAGE, "no module archive found for classpath container: " +
module.getNameAsString() + "/" + module.getVersion());
marker.setAttribute(IMarker.PRIORITY, IMarker.PRIORITY_HIGH);
marker.setAttribute(IMarker.SEVERITY, IMarker.SEVERITY_ERROR);
}
}
if (isExplodeModulesEnabled(project)) {
IClasspathEntry newEntry = newLibraryEntry(getCeylonClassesOutputFolder(project).getFullPath(),
null, null, false);
paths.put(newEntry.toString(), newEntry);
}
return asList(paths.values().toArray(new IClasspathEntry[paths.size()]));
}
public static File getSourceArtifact(RepositoryManager provider,
BaseIdeModule module) {
String sourceArchivePath = toJavaString(module.getSourceArchivePath());
if (sourceArchivePath != null) {
File sourceArchive = new File(sourceArchivePath);
if (sourceArchive.exists()) {
return sourceArchive;
}
}
// BEWARE : here the request to the provider is done in 2 steps, because if
// we do this in a single step, the Aether repo might return the .jar
// archive as a default result when not finding it with the .src extension.
// In this case it will not try the second extension (-sources.jar).
String suffix = module.getArtifactType().equals(ArtifactResultType.MAVEN) ?
ArtifactContext.LEGACY_SRC : ArtifactContext.SRC;
String namespace = module.getArtifactType().equals(ArtifactResultType.MAVEN) ?
MavenRepository.NAMESPACE : null;
ArtifactContext ctx = new ArtifactContext(namespace, module.getNameAsString(),
module.getVersion(), suffix);
File srcArtifact = null;
try {
srcArtifact = provider.getArtifact(ctx);
} catch(Exception e) {
if (e.getCause() != null && e.getCause() instanceof Exception) {
e = (Exception) e.getCause();
}
platformUtils_.get_().log(com.redhat.ceylon.ide.common.platform.Status.getStatus$_WARNING(), "Eexception during retrieval of the artifact " + ctx + " : ", e);
}
if (srcArtifact!=null) {
if (srcArtifact.getPath().endsWith(suffix)) {
return srcArtifact;
}
}
return null;
}
public static IPath getSourceArchive(RepositoryManager provider,
BaseIdeModule module) {
File srcArtifact = getSourceArtifact(provider, module);
if (srcArtifact!=null) {
if (module.getIsCeylonBinaryArchive()) {
if (module.containsJavaImplementations()) {
srcArtifact = ceylonSourceArchiveToJavaSourceArchive(
module.getNameAsString(),
module.getVersion(),
srcArtifact);
} else {
srcArtifact = null;
}
}
}
if (srcArtifact!=null) {
return new Path(srcArtifact.getAbsolutePath());
}
return null;
}
public static File getModuleArtifact(RepositoryManager provider,
BaseIdeModule module) {
if (! module.getIsSourceArchive()) {
File moduleFile = module.getArtifact();
if (moduleFile == null) {
return null;
}
if (moduleFile.exists()) {
return moduleFile;
}
}
// Shouldn't need to execute this anymore !
// We already retrieved this information during in the ModuleVisitor.
// This should be a performance gain.
ArtifactContext ctx = new ArtifactContext(null, module.getNameAsString(),
module.getVersion(), ArtifactContext.CAR);
// try first with .car
File moduleArtifact = null;
try {
moduleArtifact = provider.getArtifact(ctx);
} catch(Exception e) {
if (e.getCause() != null && e.getCause() instanceof Exception) {
e = (Exception) e.getCause();
}
platformUtils_.get_().log(com.redhat.ceylon.ide.common.platform.Status.getStatus$_WARNING(), "Exception during retrieval of the artifact " + ctx + " : ", e);
}
if (moduleArtifact==null){
// try with .jar
ctx = new ArtifactContext(null, module.getNameAsString(),
module.getVersion(), ArtifactContext.JAR);
moduleArtifact = provider.getArtifact(ctx);
}
return moduleArtifact;
}
public static IPath getModuleArchive(RepositoryManager provider,
BaseIdeModule module) {
File moduleArtifact = getModuleArtifact(provider, module);
if (moduleArtifact!=null) {
return new Path(moduleArtifact.getPath());
}
return null;
}
public static boolean isProjectModule(IJavaProject javaProject, Module module)
throws JavaModelException {
boolean isSource=false;
for (IPackageFragmentRoot s: javaProject.getPackageFragmentRoots()) {
if (s.exists()
&& javaProject.isOnClasspath(s)
&& s.getKind()==IPackageFragmentRoot.K_SOURCE
&& s.getPackageFragment(module.getNameAsString()).exists()) {
isSource=true;
break;
}
}
return isSource;
}
}