/*******************************************************************************
* Copyright (c) 2012 Pivotal Software, Inc.
* 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:
* Pivotal Software, Inc. - initial API and implementation
*******************************************************************************/
package org.grails.ide.eclipse.core.internal.classpath;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IPathVariableManager;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRoot;
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.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.jdt.core.IClasspathAttribute;
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.grails.ide.eclipse.core.GrailsCoreActivator;
import org.grails.ide.eclipse.core.internal.CharsetFixer;
import org.grails.ide.eclipse.core.internal.plugins.GrailsCore;
import org.grails.ide.eclipse.core.workspace.GrailsClassPath;
import org.grails.ide.eclipse.core.workspace.GrailsProject;
import org.grails.ide.eclipse.core.workspace.GrailsWorkspace;
import org.grails.ide.eclipse.runtime.shared.DependencyData;
/**
* This class should be renamed.
* This class refreshes the classpath of a project so that it includes
* all source folders contributed from plugins.
* @author Andrew Eisenberg
* @author Kris De Volder
* @author Andy Clement
*/
public class SourceFolderJob {
/**
* Name of the linked folder that points to the folder containing plugin source folders
* inside the .grails folder.
*/
public static final String PLUGINS_FOLDER_LINK = ".link_to_grails_plugins";
private final static boolean DEBUG = false;
private static void debug(String string) {
if (DEBUG) {
System.out.println("SourceFolderJob: "+string);
}
}
private final IClasspathAttribute[] CLASSPATH_ATTRIBUTE = new IClasspathAttribute[] { JavaCore
.newClasspathAttribute(GrailsClasspathContainer.PLUGIN_SOURCEFOLDER_ATTRIBUTE_NAME, "true") }; //$NON-NLS-1$
private final IJavaProject javaProject;
private Set<IProject> handledInPlaceProjects = new HashSet<IProject>();
// TODO FIXADE delete!!!
public SourceFolderJob(IProject project) {
this(JavaCore.create(project));
}
public SourceFolderJob(IJavaProject javaProject) {
this.javaProject = javaProject;
}
private void addToClasspath(IJavaProject javaProject,
List<IClasspathEntry> oldEntries,
List<IClasspathEntry> newEntries, IProgressMonitor monitor) {
try {
debug("addToClassPath");
debug(" old = "+oldEntries);
debug(" new = "+newEntries);
if (oldEntries.equals(newEntries)) {
debug("exit old and new are the same");
return; // Nothing to do!
}
GrailsProject grailsProject = GrailsWorkspace.get().create(javaProject);
GrailsClassPath current = grailsProject.getClassPath();
// first remove the old ones
current.removeAll(oldEntries);
// add new ones
current.addAll(newEntries);
grailsProject.setClassPath(current, monitor);
if (DEBUG) {
debug("classpath is now");
for (IClasspathEntry entry : javaProject.getRawClasspath()) {
debug(" "+entry);
}
}
cleanupLegacyLinkedSourceFolders(oldEntries);
} catch (JavaModelException e) {
GrailsCoreActivator.log(e);
}
}
public static void cleanupLegacyLinkedSourceFolders(List<IClasspathEntry> oldEntries) {
IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot();
for (IClasspathEntry entry : oldEntries) {
//Note: only entries passed here are those that are in fact plugin source entries.
// So we don't need to check that they are.
try {
IPath sourcePath = entry.getPath();
IFolder sourceFolder = wsRoot.getFolder(sourcePath);
if (sourceFolder.isLinked()) {
// In the current implementation, plugin source folders are not themselves linked source folders, they
// are instead folders nested inside a single linked folder.
// If we find any 'oldEntry' that is linked, it must be a legacy folder
debug("Deleting legacy source folder: "+sourceFolder);
sourceFolder.delete(true, new NullProgressMonitor());
}
} catch (Exception e) {
// Don't report... just don't clean it up since it obviously isn't what
// we expected.
}
}
}
/**
*
* @return all grails source class path entries. List may be empty or
* partial complete if error encountered.
*/
public static List<IClasspathEntry> getGrailsSourceClasspathEntries(IJavaProject javaProject) {
List<IClasspathEntry> oldEntries = new ArrayList<IClasspathEntry>();
try {
for (IClasspathEntry entry : javaProject.getRawClasspath()) {
// CPE_PROJECT is for in-place plugins that are present as
// project entries, as opposed to CPE_SOURCE that are for source
// folders
if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE
|| entry.getEntryKind() == IClasspathEntry.CPE_PROJECT) {
for (IClasspathAttribute attr : entry.getExtraAttributes()) {
if (attr.getName().equals(GrailsClasspathContainer.PLUGIN_SOURCEFOLDER_ATTRIBUTE_NAME)) {
oldEntries.add(entry);
}
}
}
}
} catch (JavaModelException e) {
GrailsCoreActivator.log(e);
}
return oldEntries;
}
public IStatus refreshSourceFolders(IProgressMonitor monitor) {
List<IClasspathEntry> oldEntries = getGrailsSourceClasspathEntries(javaProject);
try {
List<IClasspathEntry> newEntries = findSourceEntries(monitor);
if (newEntries.size() > 0 || oldEntries.size() > 0) {
addToClasspath(javaProject, oldEntries, newEntries, monitor);
}
fixCharSets(monitor);
} catch (CoreException e) {
return e.getStatus();
}
return Status.OK_STATUS;
}
/**
* ensure that the i18n folders have correct charsets
* @param monitor
*/
public void fixCharSets(IProgressMonitor monitor) throws CoreException {
new CharsetFixer(javaProject.getProject()).fixEncodings(new SubProgressMonitor(monitor, 2));
}
/**
* Finds all of the
* @param monitor
* @param project
* @param pluginsDirectory
* @param pluginSources
* @return
*/
public List<IClasspathEntry> findSourceEntries(IProgressMonitor monitor) throws CoreException {
IProject project = javaProject.getProject();
DependencyData data = GrailsCore.get().connect(project, PerProjectDependencyDataCache.class).getData();
if (data == null) {
throw new CoreException(new Status(IStatus.ERROR, GrailsCoreActivator.PLUGIN_ID, "Invalid plugin descriptor for " + project.getName())); //$NON-NLS-1$
}
IPath pluginsDirectory = toPath(data.getPluginsDirectory());
Set<String> pluginSources = data.getSources();
List<IClasspathEntry> newEntries = new ArrayList<IClasspathEntry>();
try {
IFolder linkToPluginsFolder = project.getFolder(PLUGINS_FOLDER_LINK);
ensureLink(linkToPluginsFolder, pluginsDirectory);
List<IClasspathEntry> newInplaceEntries = new ArrayList<IClasspathEntry>();
for (String fileDescriptor : pluginSources) {
debug("processing "+pluginSources);
IProject inPlaceProject = GrailsPluginUtil.findContainingProject(fileDescriptor);
if (inPlaceProject != null && !inPlaceProject.equals(project)) {
// In-place Grails plugins are treated specially:
// project entries are created as opposed to source folder entries
IClasspathEntry entry = createInPlaceProjectClassPath(inPlaceProject);
if (entry != null) {
debug("inplace: "+pluginSources);
newInplaceEntries.add(entry);
}
} else {
// Add source folder entry to the classpath.
// Note: classpath containers can't contain source entries.
File sourceFile = new File(fileDescriptor);
if (sourceFile.exists() && sourceFile.isDirectory()) {
debug("normal: "+pluginSources);
Path pathToSourceFolder = new Path(sourceFile.toString());
if (!pluginsDirectory.isPrefixOf(pathToSourceFolder)) {
GrailsCoreActivator.log(new Status(IStatus.ERROR, GrailsCoreActivator.PLUGIN_ID, "Broken assumption \n" +
"plugin source folder: "+pathToSourceFolder+"\n" +
"is not nested inside plugins directory: "+pluginsDirectory));
} else {
// Only execute if assumption holds: pluginsDirectory is prefix of pathToSourceFolder
IPath pluginDirRelativePath = pathToSourceFolder.removeFirstSegments(pluginsDirectory.segmentCount());
IFolder sourceFolder = linkToPluginsFolder.getFolder(pluginDirRelativePath);
newEntries.add(JavaCore.newSourceEntry(
sourceFolder.getFullPath(), null,
getExclusionsForFolder(sourceFolder),
null,
CLASSPATH_ATTRIBUTE));
}
}
else {
debug("skipped: "+sourceFile);
}
}
}
newEntries.addAll(newInplaceEntries);
} catch (Exception e) {
GrailsCoreActivator.log(e);
}
return newEntries;
}
/**
* Ensure that a given folder is a link to a given target location.
* @throws CoreException
*/
private void ensureLink(IFolder folder, IPath target) throws CoreException {
target = getPathWithVariablePrefix(target); // Try to use path variable if possible
if (folder.exists()) {
if (folder.isLinked()) {
IPath currentLocation = folder.getLocation();
if (target.equals(currentLocation)) {
folder.refreshLocal(IResource.DEPTH_INFINITE, new NullProgressMonitor());
return; //OK: It exists and links to the right place.
} else {
folder.delete(true, new NullProgressMonitor());
}
} else {
throw new CoreException(new Status(IStatus.ERROR, GrailsCoreActivator.PLUGIN_ID, "Couldn't create link because a resource already exists: "+folder));
}
}
// We should only get here if folder doesn't yet exist, or has been deleted because it was pointing to incorrect location.
folder.createLink(target, IFolder.ALLOW_MISSING_LOCAL, new NullProgressMonitor());
}
// Not used...DELETE?
// private Set<IPath> toPath(Set<String> pathStrings) {
// LinkedHashSet<IPath> paths = new LinkedHashSet<IPath>();
// for (String s : pathStrings) {
// paths.add(new Path(s));
// }
// return paths;
// }
private IPath toPath(String filePathName) {
return new Path(filePathName);
}
private Map<String, IPath> variables = null; // Caches result of getExistingPathVariables
private Map<String, IPath> getExistingPathVariables() {
if (variables==null) {
variables = new HashMap<String, IPath>();
IPathVariableManager variableManager = ResourcesPlugin.getWorkspace().getPathVariableManager();
for (String variableName : variableManager.getPathVariableNames()) {
IPath path = variableManager.getValue(variableName);
File file = path.toFile();
if (file.exists() && file.isDirectory()) {
variables.put(variableName, path);
}
}
}
return variables;
}
/**
* Creates a class path entry for an in-place plugin dependency. The
* entry should be of CPE_PROJECT kind.
*/
protected IClasspathEntry createInPlaceProjectClassPath(IProject project) {
IJavaProject javaProject = JavaCore.create(project);
if (javaProject == null || handledInPlaceProjects.contains(project)) {
return null;
}
handledInPlaceProjects.add(project);
return JavaCore.newProjectEntry(project.getFullPath(), null, true,
CLASSPATH_ATTRIBUTE, true);
}
/**
* Return a new IPath that contains the original sourcePath with leading
* segments replaced with an appropriate Path variable. If no variables
* are substituted, return the original path.
*
* @param variables
* containing all Path variables in workspace
* @param sourcePath
* that needs to have leading segments replaced with a Path
* variable
* @return new IPath with Path variable, or null if no changes
*/
protected IPath getPathWithVariablePrefix(IPath sourcePath) {
Map<String, IPath> variables = getExistingPathVariables();
if (variables == null || sourcePath == null) {
return sourcePath;
}
// Check if we have a matching variable defined
IPath sourcePathWithVariable = null;
int segmentMatch = 0;
for (Map.Entry<String, IPath> entry : variables.entrySet()) {
if (entry.getValue().isPrefixOf(sourcePath)) {
// We have a match; now check that we
// use the most specific match
int currentSegmentMatch = sourcePath
.matchingFirstSegments(entry.getValue());
if (segmentMatch < currentSegmentMatch) {
sourcePathWithVariable = new Path(entry.getKey())
.append(sourcePath.removeFirstSegments(entry
.getValue().segmentCount()));
segmentMatch = currentSegmentMatch;
}
}
}
return sourcePathWithVariable==null ? sourcePath : sourcePathWithVariable;
}
/**
* Return the set of exclusions that should apply to the named folder.
* The exclusions follow the guidelines in
* grails.util.PluginBuildSettings.EXCLUDED_RESOURCES. These resources
* should be excluded from the linked source folder because they would
* not be part of the packaged plugin. These resources only need
* filtering for local plugins that are linking to folders in another
* project in the workspace. If installing a non-local plugin it is
* guaranteed not to have these things in anyway.<br>
*
* As of grails 1.3.1, the exclusions are: public static final
* EXCLUDED_RESOURCES = [ "web-app/WEB-INF/**", "web-app/plugins/**",
* "grails-app/conf/spring/resources.groovy",
* "grails-app/conf/*DataSource.groovy",
* "grails-app/conf/DataSource.groovy",
* "grails-app/conf/BootStrap.groovy", "grails-app/conf/Config.groovy",
* "grails-app/conf/BuildConfig.groovy",
* "grails-app/conf/UrlMappings.groovy", "**<slash>.svn<slash>**",
* "test/**", "**<slash>CVS<slash>**" ]
*/
private IPath[] getExclusionsForFolder(IFolder sourceFolder) {
IPath sourcePath = sourceFolder.getFullPath();
if (sourcePath!=null && sourcePath.segmentCount()>0) {
if (sourcePath.lastSegment().equals("conf")) {
return exclusionsForConfFolder;
}
if (sourcePath.segmentCount()>=3) {
//path looks like "/<project>/.link-to-plugins/<plugin-name>-<version>/...
String pluginNameAndVersion = sourcePath.segment(2);
if (pluginNameAndVersion.startsWith("spring-security-acl-") && sourcePath.lastSegment().equals("domain")) {
return getExclusionsForSpringSecurityAcl();
}
}
}
return null;
}
/**
* Relates to https://issuetracker.springsource.com/browse/STS-1832 and
* https://issuetracker.springsource.com/browse/STS-1799
*/
private IPath[] getExclusionsForSpringSecurityAcl() {
IFolder folder = javaProject.getProject().getFolder("grails-app/domain/org/codehaus/groovy/grails/plugins/springsecurity/acl");
if (!folder.exists()) {
//Return quickly... classes not copied!
return null;
} else {
ArrayList<IPath> exclusions = new ArrayList<IPath>(exclusionsForSpringSecurityAclDomain.length);
for (IPath exclude : exclusionsForSpringSecurityAclDomain) {
//looks like **/<some-groovy-file>
String fileName = exclude.segment(1);
IFile copiedFile = folder.getFile(fileName);
if (copiedFile.exists()) {
exclusions.add(exclude);
}
}
return exclusions.toArray(new IPath[exclusions.size()]);
}
}
private IPath[] exclusionsForConfFolder = new IPath[] {
new Path("BuildConfig.groovy"), new Path("*DataSource.groovy"), //$NON-NLS-1$ //$NON-NLS-2$
new Path("UrlMappings.groovy"), new Path("Config.groovy"), //$NON-NLS-1$ //$NON-NLS-2$
new Path("BootStrap.groovy"), //$NON-NLS-1$
new Path("spring/resources.groovy") }; //$NON-NLS-1$
private IPath[] exclusionsForSpringSecurityAclDomain = new IPath[] {
new Path("**/AclClass.groovy"),
new Path("**/AclEntry.groovy"),
new Path("**/AclObjectIdentity.groovy"),
new Path("**/AclSid.groovy")
};
}