package com.redhat.ceylon.eclipse.core.external;
import static com.redhat.ceylon.eclipse.util.InteropUtils.toJavaString;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.Vector;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.internal.resources.Resource;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
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.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IResourceStatus;
import org.eclipse.core.resources.IResourceVisitor;
import org.eclipse.core.resources.IWorkspace;
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.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jdt.internal.core.util.Messages;
import org.eclipse.jdt.internal.core.util.Util;
import com.redhat.ceylon.cmr.api.ArtifactContext;
import com.redhat.ceylon.eclipse.core.builder.CeylonBuilder;
import com.redhat.ceylon.eclipse.core.builder.CeylonNature;
import com.redhat.ceylon.eclipse.ui.CeylonPlugin;
import com.redhat.ceylon.ide.common.model.BaseIdeModule;
public class ExternalSourceArchiveManager implements IResourceChangeListener {
private static final String EXTERNAL_PROJECT_NAME = "Ceylon Source Archives";
private static final String LINKED_FOLDER_NAME = "archive-";
private Map<IPath, IResource> archives;
private Set<IPath> pendingSourceArchives; // subset of keys of 'archives', for which linked folders haven't been created yet.
/* Singleton instance */
private static ExternalSourceArchiveManager MANAGER;
static {
MANAGER = new ExternalSourceArchiveManager();
}
private ExternalSourceArchiveManager() {
}
public static ExternalSourceArchiveManager getExternalSourceArchiveManager() {
return MANAGER;
}
/*
* Returns a set of external path to external folders referred to on the given classpath.
* Returns null if none.
*/
public static Set<IPath> getExternalSourceArchives(Collection<BaseIdeModule> modules) {
if (modules == null)
return Collections.emptySet();
Set<IPath> folders = null;
for (BaseIdeModule module : modules) {
if (module.getIsCeylonArchive()) {
IPath archivePath = Path.fromOSString(toJavaString(module.getSourceArchivePath()));
if (isExternalSourceArchivePath(archivePath)) {
if (folders == null)
folders = new HashSet<>();
folders.add(archivePath);
}
}
}
return folders != null ?
folders : Collections.<IPath>emptySet();
}
public static boolean isExternalSourceArchivePath(IPath externalPath) {
if (externalPath == null)
return false;
File externalSourceArchive = externalPath.toFile();
if (! externalSourceArchive.isFile())
return false;
if (! externalSourceArchive.getName().endsWith(ArtifactContext.SRC))
return false;
if (!externalSourceArchive.exists())
return false;
return true;
}
public static boolean isInternalPathForExternalSourceArchive(IPath resourcePath) {
return EXTERNAL_PROJECT_NAME.equals(resourcePath.segment(0));
}
public IFolder addSourceArchive(IPath externalSourceArchivePath, boolean scheduleForCreation) {
return addSourceArchive(externalSourceArchivePath, getExternalSourceArchivesProject(), scheduleForCreation);
}
private IFolder addSourceArchive(IPath externalSourceArchivePath, IProject externalSourceArchivesProject, boolean scheduleForCreation) {
if (archives == null) {
return null;
}
IResource existing = archives.get(externalSourceArchivePath);
if (existing != null && existing.exists()) {
return (IFolder) existing;
}
IFolder result = externalSourceArchivesProject.getFolder(new Path(externalSourceArchivePath.toString() + "!"));
if (scheduleForCreation) {
synchronized(this) {
if (pendingSourceArchives == null)
pendingSourceArchives = Collections.synchronizedSet(new HashSet<IPath>());
}
pendingSourceArchives.add(externalSourceArchivePath);
}
archives.put(externalSourceArchivePath, 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 removePendingSourceArchive(Object externalPath) {
if (this.pendingSourceArchives == null)
return false;
return this.pendingSourceArchives.remove(externalPath);
}
public IFolder createLinkFolder(IPath externalSourceArchivePath, boolean refreshIfExistAlready, IProgressMonitor monitor) throws CoreException {
IProject externalSourceArchivesProject = createExternalSourceArchivesProject(monitor); // run outside synchronized as this can create a resource
return createLinkFolder(externalSourceArchivePath, refreshIfExistAlready, externalSourceArchivesProject, monitor);
}
private void createVirtualFolderIfNecessary(IContainer container, IProgressMonitor monitor) throws CoreException {
if (container instanceof IFolder && ! container.exists()) {
createVirtualFolderIfNecessary(container.getParent(), monitor);
((IFolder) container).create(IResource.VIRTUAL, false, monitor);
}
}
private IFolder createLinkFolder(IPath externalSourceArchivePath, boolean refreshIfExistAlready,
IProject externalSourceArchivesProject, IProgressMonitor monitor) throws CoreException {
IFolder result = addSourceArchive(externalSourceArchivePath, externalSourceArchivesProject, false);
URI uri = result.getLocationURI();
if (uri == null || !CeylonArchiveFileSystem.SCHEME_CEYLON_ARCHIVE.equals(uri.getScheme())) {
createVirtualFolderIfNecessary(result.getParent(), monitor);
if (result.exists()) {
result.delete(true, monitor);
}
result.createLink(CeylonArchiveFileSystem.toCeylonArchiveURI(externalSourceArchivePath, Path.EMPTY), IResource.ALLOW_MISSING_LOCAL, monitor);
}
else if (refreshIfExistAlready)
result.refreshLocal(IResource.DEPTH_INFINITE, monitor);
return result;
}
public void createPendingSourceArchives(IProgressMonitor monitor) throws CoreException{
synchronized (this) {
if (pendingSourceArchives == null || pendingSourceArchives.isEmpty()) return;
}
IProject externalSourceArchivesProject = null;
externalSourceArchivesProject = createExternalSourceArchivesProject(monitor);
// To avoid race condition (from addSourceArchive and removeSourceArchive, 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[] arrayOfSourceArchives = null;
synchronized (pendingSourceArchives) {
arrayOfSourceArchives = pendingSourceArchives.toArray();
pendingSourceArchives.clear();
}
for (int i=0; i < arrayOfSourceArchives.length; i++) {
try {
createLinkFolder((IPath) arrayOfSourceArchives[i], false, externalSourceArchivesProject, monitor);
} catch (CoreException e) {
Util.log(e, "Error while creating a link for external folder :" + arrayOfSourceArchives[i]); //$NON-NLS-1$
}
}
}
private void deleteVirtualFolderIfPossible(IContainer container, IProgressMonitor monitor) {
try {
if (container.exists()
&& container instanceof IFolder
&& container.isVirtual()
&& container.members().length == 0) {
container.delete(false, monitor);
deleteVirtualFolderIfPossible(container.getParent(), monitor);
}
} catch (CoreException e) {
e.printStackTrace();
}
}
public void cleanUp(IProgressMonitor monitor) throws CoreException {
ArrayList<Entry<IPath, IResource>> toDelete = getSourceArchivesToCleanUp(monitor);
if (toDelete == null)
return;
for (Iterator<Entry<IPath, IResource>> iterator = toDelete.iterator(); iterator.hasNext();) {
Entry<IPath, IResource> entry = iterator.next();
IFolder folder = (IFolder) entry.getValue();
folder.delete(true, monitor);
deleteVirtualFolderIfPossible(folder.getParent(), monitor);
IPath key = (IPath) entry.getKey();
archives.remove(key);
}
IProject project = getExternalSourceArchivesProject();
if (project.isAccessible() && project.members().length == 1/*remaining member is .project*/)
project.delete(true, monitor);
}
private ArrayList<Entry<IPath, IResource>> getSourceArchivesToCleanUp(IProgressMonitor monitor) throws CoreException {
if (archives == null) {
return null;
}
Set<IPath> projectSourcePaths = null;
for (IProject project : CeylonBuilder.getProjects()) {
for (BaseIdeModule module : CeylonBuilder.getProjectExternalModules(project)) {
String sourceArchivePathString = toJavaString(module.getSourceArchivePath());
if (sourceArchivePathString!= null) {
if (projectSourcePaths == null) {
projectSourcePaths = new HashSet<>();
}
projectSourcePaths.add(Path.fromOSString(sourceArchivePathString));
}
}
}
if (projectSourcePaths == null)
return null;
ArrayList<Entry<IPath, IResource>> result = null;
synchronized (archives) {
Iterator<Entry<IPath, IResource>> iterator = archives.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<IPath, IResource> entry = iterator.next();
IPath path = entry.getKey();
if (!projectSourcePaths.contains(path)) {
if (entry.getValue() != null) {
if (result == null)
result = new ArrayList<>();
result.add(entry);
}
}
}
}
return result;
}
public IProject getExternalSourceArchivesProject() {
return ResourcesPlugin.getWorkspace().getRoot().getProject(EXTERNAL_PROJECT_NAME);
}
public IProject createExternalSourceArchivesProject(IProgressMonitor monitor) throws CoreException {
IProject project = getExternalSourceArchivesProject();
if (!project.isAccessible()) {
if (!project.exists()) {
createExternalSourceArchivesProject(project, monitor);
}
openExternalSourceArchivesProject(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 openExternalSourceArchivesProject(IProject project, IProgressMonitor monitor) throws CoreException {
try {
project.open(monitor);
} catch (CoreException e1) {
if (e1.getStatus().getCode() == IResourceStatus.FAILED_READ_METADATA) {
// workspace was moved
project.delete(false/*don't delete content*/, true/*force*/, monitor);
createExternalSourceArchivesProject(project, monitor);
} else {
// .project or folder on disk have been deleted, recreate them
IPath stateLocation = CeylonPlugin.getInstance().getStateLocation();
IPath projectPath = stateLocation.append(EXTERNAL_PROJECT_NAME);
projectPath.toFile().mkdirs();
try {
FileOutputStream output = new FileOutputStream(projectPath.append(".project").toOSString()); //$NON-NLS-1$
try {
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$
} finally {
output.close();
}
} catch (IOException e) {
// fallback to re-creating the project
project.delete(false/*don't delete content*/, true/*force*/, monitor);
createExternalSourceArchivesProject(project, monitor);
}
}
project.open(monitor);
}
}
private void createExternalSourceArchivesProject(IProject project, IProgressMonitor monitor) throws CoreException {
IProjectDescription desc = project.getWorkspace().newProjectDescription(project.getName());
IPath stateLocation = CeylonPlugin.getInstance().getStateLocation();
desc.setLocation(stateLocation.append(EXTERNAL_PROJECT_NAME));
project.create(desc, IResource.HIDDEN, monitor);
}
public IFolder getSourceArchive(IPath externalSourceArchivePath) {
return (IFolder) (archives == null ? null : archives.get(externalSourceArchivePath));
}
public void initialize() {
final Map<IPath, IResource> tempSourceArchives = new HashMap<>();
IProject project = getExternalSourceArchivesProject();
try {
doInitialize(tempSourceArchives, project);
for (IResource value : tempSourceArchives.values()) {
if (value.getName().startsWith(LINKED_FOLDER_NAME)) {
try {
project.delete(true, null);
openExternalSourceArchivesProject(project, null);
doInitialize(tempSourceArchives, project);
} catch (CoreException e) {
e.printStackTrace();
}
}
}
} catch (CoreException e) {
Util.log(e, "Exception while initializing external folders");
}
archives = Collections.synchronizedMap(tempSourceArchives);
for (final IProject ceylonProject : CeylonBuilder.getProjects()) {
if (CeylonBuilder.isContainerInitialized(ceylonProject)) {
// the project container was already initialized
// => restarts an update of the source archives
Job refreshProjectExternalSourceArchive = new Job("Update External Ceylon Sources Archives for project " + ceylonProject.getName()) {
protected IStatus run(IProgressMonitor monitor) {
ExternalSourceArchiveManager esam = ExternalSourceArchiveManager.getExternalSourceArchiveManager();
try {
esam.updateProjectSourceArchives(ceylonProject, monitor);
} catch (CoreException e) {
e.printStackTrace();
}
return Status.OK_STATUS;
};
};
refreshProjectExternalSourceArchive.setRule(ResourcesPlugin.getWorkspace().getRoot());
refreshProjectExternalSourceArchive.schedule();
}
}
}
protected void doInitialize(final Map<IPath, IResource> tempSourceArchives,
IProject project) throws CoreException {
if (!project.isAccessible()) {
if (project.exists()) {
// workspace was moved
openExternalSourceArchivesProject(project, null/*no progress*/);
} else {
// if project doesn't exist, do not open and recreate it as it means that there are no external source archives
return;
}
}
project.accept(new IResourceVisitor() {
@Override
public boolean visit(IResource resource) throws CoreException {
if (resource instanceof IFolder
&& resource.isLinked()
&& ! resource.isVirtual()
&& resource.isSynchronized(IResource.DEPTH_ZERO)
&& resource.exists()) {
URI uri = resource.getLocationURI();
if (uri != null && CeylonArchiveFileSystem.SCHEME_CEYLON_ARCHIVE.equals(uri.getScheme())) {
String path = uri.getPath();
if (path != null) {
if (path.endsWith(CeylonArchiveFileSystem.JAR_SUFFIX)) {
path = path.substring(0, path.length() - 2);
}
IPath externalSourceArchivePath = new Path(path);
tempSourceArchives.put(externalSourceArchivePath, resource);
}
}
return false;
}
return true;
}
}, IResource.DEPTH_INFINITE, IContainer.INCLUDE_HIDDEN);
}
private void runRefreshJob(Collection<IPath> paths) {
Job[] jobs = Job.getJobManager().find(ResourcesPlugin.FAMILY_MANUAL_REFRESH);
RefreshJob refreshJob = null;
if (jobs != null) {
for (int index = 0; index < jobs.length; index++) {
// We are only concerned about ExternalSourceArchiveManager.RefreshJob
if(jobs[index] instanceof RefreshJob) {
refreshJob = (RefreshJob) jobs[index];
refreshJob.addSourceArchivesToRefresh(paths);
if (refreshJob.getState() == Job.NONE) {
refreshJob.schedule();
}
break;
}
}
}
if (refreshJob == null) {
refreshJob = new RefreshJob(new Vector<>(paths));
refreshJob.schedule();
}
}
/*
* Refreshes the external folders referenced on the classpath of the given source project
*/
public void refreshReferences(final IProject[] sourceProjects, IProgressMonitor monitor) {
IProject externalProject = getExternalSourceArchivesProject();
Set<IPath> externalSourceArchives = null;
for (IProject project : sourceProjects) {
if (project.equals(externalProject))
continue;
if (!CeylonNature.isEnabled(project))
continue;
Set<IPath> sourceArchivesInProject = getExternalSourceArchives(CeylonBuilder.getProjectExternalModules(project));
if (sourceArchivesInProject.isEmpty())
continue;
if (externalSourceArchives == null)
externalSourceArchives = new HashSet<>();
externalSourceArchives.addAll(sourceArchivesInProject);
}
if (externalSourceArchives == null)
return;
runRefreshJob(externalSourceArchives);
}
public void refreshReferences(IProject source, IProgressMonitor monitor) {
IProject externalProject = getExternalSourceArchivesProject();
if (source.equals(externalProject))
return;
if (!CeylonNature.isEnabled(source))
return;
Set<IPath> externalSourceArchives = getExternalSourceArchives(CeylonBuilder.getProjectExternalModules(source));
if (externalSourceArchives.isEmpty())
return;
runRefreshJob(externalSourceArchives);
return;
}
public IFolder removeSourceArchive(IPath externalSourceArchivePath) {
return (IFolder) (archives == null ? null : archives.remove(externalSourceArchivePath));
}
class RefreshJob extends Job {
Vector<IPath> externalSourceArchives = null;
RefreshJob(Vector<IPath> externalSourceArchives){
super(Messages.refreshing_external_folders);
this.externalSourceArchives = externalSourceArchives;
}
public boolean belongsTo(Object family) {
return family == ResourcesPlugin.FAMILY_MANUAL_REFRESH;
}
/*
* Add the collection of paths to be refreshed to the already
* existing list of paths.
*/
public void addSourceArchivesToRefresh(Collection<IPath> paths) {
if (!paths.isEmpty() && externalSourceArchives == null) {
externalSourceArchives = new Vector<IPath>();
}
Iterator<IPath> it = paths.iterator();
while(it.hasNext()) {
IPath path = it.next();
if (!externalSourceArchives.contains(path)) {
externalSourceArchives.add(path);
}
}
}
protected IStatus run(IProgressMonitor pm) {
try {
if (externalSourceArchives == null)
return Status.OK_STATUS;
IPath externalPath = null;
for (int index = 0; index < externalSourceArchives.size(); index++ ) {
if ((externalPath = externalSourceArchives.get(index)) != null) {
IFolder sourceArchive = getSourceArchive(externalPath);
if (sourceArchive != null)
sourceArchive.refreshLocal(IResource.DEPTH_INFINITE, pm);
}
// Set the processed ones to null instead of removing the element altogether,
// so that they will not be considered as duplicates.
// This will also avoid elements being shifted to the left every time an element
// is removed. However, there is a risk of Collection size to be increased more often.
externalSourceArchives.setElementAt(null, index);
}
} catch (CoreException e) {
return e.getStatus();
}
return Status.OK_STATUS;
}
}
@Override
public void resourceChanged(IResourceChangeEvent event) {
switch(event.getType()) {
case IResourceChangeEvent.PRE_REFRESH :
IProject [] projects = null;
Object o = event.getSource();
if (o instanceof IProject) {
projects = new IProject[] { (IProject) o };
} else if (o instanceof IWorkspace) {
// The single workspace refresh
// notification we see, implies that all projects are about to be refreshed.
projects = ((IWorkspace) o).getRoot().getProjects(IContainer.INCLUDE_HIDDEN);
}
// Refresh all project references together in a single job
refreshReferences(projects, null);
return;
}
}
public static IPath getSourceArchiveFullPath(IFolder sourceArchiveFolder) {
if (MANAGER.archives != null) {
for (Entry<IPath, IResource> entry : MANAGER.archives.entrySet()) {
if (entry.getValue().equals(sourceArchiveFolder)) {
return entry.getKey();
}
}
}
return null;
}
public static boolean isInSourceArchive(IResource resource) {
return resource != null && resource.getProject().equals(getExternalSourceArchiveManager().getExternalSourceArchivesProject());
}
public static boolean isTheSourceArchiveProject(IProject project) {
return project != null && project.equals(getExternalSourceArchiveManager().getExternalSourceArchivesProject());
}
public static IPath toFullPath(IResource resource) {
if (resource == null) {
return null;
}
IProject project = resource.getProject();
if (! project.equals(MANAGER.getExternalSourceArchivesProject())) {
return null;
}
if (resource.isSynchronized(IResource.DEPTH_ZERO)
&& resource.exists()) {
IPath path = new Path(resource.getLocationURI().getPath());
if (path != null && path.toString().contains(".src!")) {
return path;
}
}
return null;
}
public static IResource toResource(IPath sourceArchiveEntryPath) {
String entryPathString = sourceArchiveEntryPath.toString();
int jarSuffixIndex = entryPathString.indexOf(CeylonArchiveFileSystem.JAR_SUFFIX);
if (jarSuffixIndex > 0) {
IPath archivePath = new Path(entryPathString.substring(0, jarSuffixIndex));
IFolder sourceArchiveFolder = getExternalSourceArchiveManager().getSourceArchive(archivePath);
if (sourceArchiveFolder != null) {
IPath entryPath = new Path(entryPathString.substring(jarSuffixIndex + 2));
IResource resource = sourceArchiveFolder.findMember(entryPath);
return resource;
}
}
return null;
}
public static IResource toResource(URI sourceArchiveEntryURI) {
String scheme = sourceArchiveEntryURI.getScheme();
if (EFS.SCHEME_FILE.equals(scheme) ||
CeylonArchiveFileSystem.SCHEME_CEYLON_ARCHIVE.equals(scheme)) {
return toResource(new Path(sourceArchiveEntryURI.getPath()));
}
return null;
}
public void updateProjectSourceArchives(IProject project, IProgressMonitor monitor) throws CoreException {
if (archives != null) {
if (CeylonBuilder.allClasspathContainersInitialized()) {
cleanUp(monitor);
}
for (IPath sourceArchivePath : getExternalSourceArchives(CeylonBuilder.getProjectExternalModules(project))) {
IFolder sourceArchive = getSourceArchive(sourceArchivePath);
if (sourceArchive == null || !sourceArchive.exists()) {
addSourceArchive(sourceArchivePath, true);
}
}
createPendingSourceArchives(monitor);
}
}
public static IPath getSourceArchiveEntryPath(IFile file) {
IPath relativePath = null;
IFileStore store = ((Resource) file).getStore();
if (store instanceof CeylonArchiveFileStore) {
CeylonArchiveFileStore cafs = (CeylonArchiveFileStore) store;
relativePath = cafs.getEntryPath();
}
return relativePath;
}
}