/******************************************************************************* * 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.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Set; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.FileLocator; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Platform; import org.eclipse.jdt.core.IAccessRule; import org.eclipse.jdt.core.IClasspathAttribute; 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.grails.ide.eclipse.core.GrailsCoreActivator; import org.grails.ide.eclipse.core.internal.plugins.GrailsCore; import org.grails.ide.eclipse.core.model.GrailsBuildSettingsException; import org.grails.ide.eclipse.core.model.GrailsVersion; import org.grails.ide.eclipse.core.model.IGrailsInstall; import org.grails.ide.eclipse.runtime.shared.DependencyData; import org.osgi.framework.Bundle; import org.springsource.ide.eclipse.commons.core.SpringCorePreferences; /** * @author Christian Dupuis * @author Andrew Eisenberg * @author Andy Clement * @author Nieraj Singh * @author Kris De Volder * @since 2.2.0 */ public class GrailsClasspathContainer implements IClasspathContainer { /** * DSL support folder relative to the current grails install */ private static final String DSL_SUPPORT_FOLDER = "dsl-support"; public static final String PLUGIN_SOURCEFOLDER_ATTRIBUTE_NAME = "org.grails.ide.eclipse.core.SOURCE_FOLDER"; public static final String CLASSPATH_ATTRIBUTE_VALUE = GrailsCoreActivator.PLUGIN_ID + ".CLASSPATH_ENTRY"; public final static IClasspathAttribute[] NO_EXTRA_ATTRIBUTES = {}; public final static IAccessRule[] NO_ACCESS_RULES = {}; /** Classpath container */ public static final String CLASSPATH_CONTAINER_DESCRIPTION = "Grails Dependencies"; /** Name of this class path container to be stored by JDT */ private static final String CLASSPATH_CONTAINER = GrailsCoreActivator.PLUGIN_ID + ".CLASSPATH_CONTAINER"; /** Unique path of this class path container */ public static final IPath CLASSPATH_CONTAINER_PATH = new Path(CLASSPATH_CONTAINER); private String description = ""; /** The calculated and stored {@link IClasspathEntry}s */ private IClasspathEntry[] entries; /** The internal flag to indicate the container has been initialized */ private volatile boolean initialized = false; /** * The {@link IJavaProject} this class path container instance is responsible for */ IJavaProject javaProject; /** Set of full file paths to plugin.xml files */ private Set<String> pluginDescriptors; private GrailsVersion grailsVersion = GrailsVersion.UNKNOWN; /** * Constructor to create a new class path container * * @param javaProject the {@link IJavaProject} that this container is responsible for */ public GrailsClasspathContainer(IJavaProject javaProject) { this.javaProject = javaProject; this.entries = new IClasspathEntry[0]; } /** * Constructor to create a new class path container * * @param javaProject the {@link IJavaProject} that this container is responsible for * @param entries populate the list of {@link IClasspathEntry}s with the given list */ public GrailsClasspathContainer(IJavaProject javaProject, IClasspathEntry[] entries) { this.javaProject = javaProject; this.entries = entries; } /** * Returns the {@link IClasspathEntry}s calculated by this class path container */ public IClasspathEntry[] getClasspathEntries() { synchronized (GrailsCore.get().getLockForProject(javaProject.getProject())) { // make sure that the container is initialized on first access if (!initialized) { // refresh container before giving out the empty entries list refreshClasspathEntries(); } return this.entries; } } /** * Returns the description for this class path container */ public String getDescription() { return CLASSPATH_CONTAINER_DESCRIPTION; } public String getDescriptionSuffix() { return description; } /** * Returns the kind of this class path container */ public int getKind() { return K_APPLICATION; } /** * Returns the path of the class path container */ public IPath getPath() { return CLASSPATH_CONTAINER_PATH; } public synchronized void invalidate() { entries = null; initialized = false; } /** * Refresh the class path entries of the given {@link IJavaProject}. * <p> * This will install the new class path entries on the java project only if the entries have changed since the last * refresh. */ private void refreshClasspathEntries() { final List<IClasspathEntry> entries = new ArrayList<IClasspathEntry>(); IProject project = javaProject.getProject(); try { PerProjectDependencyDataCache info = GrailsCore.get().connect(project, PerProjectDependencyDataCache.class); PerProjectAttachementsCache attachements = PerProjectAttachementsCache.get(project); IGrailsInstall install = GrailsCoreActivator.getDefault().getInstallManager() .getGrailsInstall(javaProject.getProject()); try { // info can null if classpath container is a on project without the nature; so be extra careful if (info != null && info.getData() != null) { DependencyData data = info.getData(); // These are the paths to the source folders // Class path entries are created from this data Set<String> dependencies = data.getDependencies(); // These are the locations for the plugin.xml files, but // are NOT used to create class path entries. They are simply // parsed at the same time as the dependencies to ensure // that whoever requests descriptors will be getting them // from the same class path container that also created the // class path entries, since both pieces of information // are obtained from the same dependency parser this.pluginDescriptors = data.getPluginDescriptors(); for (String fileDescriptor : dependencies) { // don't link in .svn and CVS folders; would eventually make // sense to not link in folders starting // with '.'; see STS-745 File file = new File(fileDescriptor); String name = file.getName(); if (name.startsWith(".svn") || name.equals("CVS")) { continue; } if (!(file.exists() && file.canRead())) { continue; } IPath sourceFile = getSourceAttachement(project, attachements, file); IClasspathAttribute[] attributes = NO_EXTRA_ATTRIBUTES; String javaDocPath = GrailsClasspathContainer.getJavaDocLocation(project, file); if (javaDocPath != null) { attributes = new IClasspathAttribute[] { JavaCore.newClasspathAttribute( IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME, javaDocPath) }; } entries.add(JavaCore.newLibraryEntry(new Path(file.getAbsolutePath()), sourceFile, null, NO_ACCESS_RULES, attributes, false)); } //STS-2084: temporary workaround, revisit after M2 is no longer interesting //Note: still a problem in build snapshot since M2 but should reevaluate on each milestone release if (install != null && GrailsVersion.V_1_3_7.compareTo(install.getVersion())<=0 && GrailsVersion.V_2_0_3.compareTo(install.getVersion())>0) { //if (GrailsNature.isGrailsPluginProject(project)) { File ivyLib = getIvyLib(install); if (ivyLib!=null) { entries.add(JavaCore.newLibraryEntry(new Path(ivyLib.getAbsolutePath()), null, null, NO_ACCESS_RULES, NO_EXTRA_ATTRIBUTES, false)); } //} } //At the very end at the 'plugin-classes' directory as a library String pluginClassesStr = data.getPluginClassesDirectory(); if (pluginClassesStr!=null) { File pluginClassesDir = new File(pluginClassesStr); if (pluginClassesDir.exists()) { entries.add(JavaCore.newLibraryEntry(Path.fromOSString(pluginClassesStr), null, null)); } } } else { //Dependency file doesn't exist and we cannot build it now, since that takes too long. description += " (uninitialized)"; //The project will get compiled by JDT with errors. See STS-1347 //We must make sure that sometime soon the dependencies are going to be refreshed: if (!isInWonkyState(javaProject)) { //Don't auto-refresh projects that are in a 'wonky' state. This will probably just cause spurious errors //in the error log, as well work that need not be done at all. GrailsClasspathContainerUpdateJob.scheduleClasspathContainerUpdateJob(javaProject, false); } } } finally { if (install != null) { description = " [" + install.getName() + "]"; this.grailsVersion = install.getVersion(); } else { this.grailsVersion = GrailsVersion.UNKNOWN; } } } catch (GrailsBuildSettingsException e) { GrailsCoreActivator.log("Issue with external Grails installation", e); } finally { // add the grails.dsld here. It is appropriate to place it outside of the try-catch block // since it should be added regardless of whether or not the rest of the classpath container // was built successfully. IClasspathEntry dsldClasspathEntry = findGrailsDsld(); if (dsldClasspathEntry != null) { entries.add(dsldClasspathEntry); } this.entries = entries.toArray(new IClasspathEntry[entries.size()]); this.initialized = true; } } /** * When project is in a 'wonky' state we don't do auto-refreshes because it can cause much work and spurious errors. * Since project is in wonky state it probably already needs repairing so even if the refresh succeeds it * probably populates the container with the wrong information! So this work is just not worth it. */ private boolean isInWonkyState(IJavaProject project) { try { GrailsVersion eclipseVersion = GrailsVersion.getEclipseGrailsVersion(project.getProject()); GrailsVersion grailVersion = GrailsVersion.getGrailsVersion(project.getProject()); boolean looksFine = !eclipseVersion.equals(GrailsVersion.UNKNOWN) && eclipseVersion.equals(grailVersion); return !looksFine; } catch (Exception e) { GrailsCoreActivator.log(e); return true; //Errors where caught. Not sure why but we'll call that wonky too :-) } } private File getIvyLib(IGrailsInstall install) { String[] locations = { "lib/ivy-2.2.0.jar", //1.3.x "lib/org.apache.ivy/ivy/jars/ivy-2.2.0.jar", //2.0.1 and 2.0.3 "lib/org.apache.ivy/ivy/2.2.0/jar/ivy-2.2.0.jar" //2.0.2 }; for (String loc : locations) { File file = new File(install.getHome(), loc); if (file.exists()) { return file; } } return null; } public IPath getSourceAttachement(IProject project, PerProjectAttachementsCache attachements, File file) { IPath sourceFile = getSourceAttachmentFromUserPrefs(project, file); if (sourceFile == null) { sourceFile = attachements.getSourceAttachement(file.toString()); if (sourceFile==null) { sourceFile = getDefaultSourceAttachment(project, file); } } return sourceFile; } /** * Returns the classpath entry corresponding to the external location where * the grails.dsld is located. This is in the c.s.s.grails.resources plugin * @return the classpath entry or null if it does not exist */ private IClasspathEntry findGrailsDsld() { IGrailsInstall install = GrailsVersion.getEclipseGrailsVersion(javaProject.getProject()).getInstall(); // only use for 2.0 or greater if (install != null && install.getVersion().compareTo(GrailsVersion.V_1_3_7) > 0) { Bundle b = Platform.getBundle(GrailsCoreActivator.GRAILS_RESOURCES_PLUGIN_ID); if (b != null) { URL entry = b.getEntry(DSL_SUPPORT_FOLDER); URL resolvedEntry; try { resolvedEntry = FileLocator.resolve(entry); IPath jarPath = new Path(resolvedEntry.getPath()); return JavaCore.newLibraryEntry(jarPath, null, null); } catch (IOException e) { GrailsCoreActivator.log(e); } } } return null; } public Set<String> getPluginDescriptors() { return pluginDescriptors; } /** * Stores the configured source attachments paths in the projects settings area. * @param project the java project to store the preferences for * @param containerSuggestion the configured classpath container entries */ public static void storeSourceAttachments(IJavaProject project, IClasspathContainer containerSuggestion) { SpringCorePreferences prefs = SpringCorePreferences.getProjectPreferences(project.getProject(), GrailsCoreActivator.PLUGIN_ID); for (IClasspathEntry entry : containerSuggestion.getClasspathEntries()) { IPath path = entry.getPath(); IPath sourcePath = entry.getSourceAttachmentPath(); if (sourcePath != null) { prefs.putString("source.attachment-" + path.lastSegment().toString(), sourcePath.toString()); } for (IClasspathAttribute attribute : entry.getExtraAttributes()) { if (attribute.getName().equals(IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME)) { String value = attribute.getValue(); prefs.putString("javadoc.location-" + path.lastSegment().toString(), value); } } } } /** * Returns the default source attachment for a given jar, if one exists. Should only exist for the grails jars * @param project the java project which preferences needs to be checked. * @param file the jar that needs a source attachment * @return path to the default source attachment or null if none exists */ public static IPath getDefaultSourceAttachment(IProject project, File file) { if (file.getName().startsWith("grails-")) { IGrailsInstall install = GrailsCoreActivator.getDefault().getInstallManager().getGrailsInstall(project); if (install!=null) { if (file.getName().startsWith("grails-script")) { // STS-1778: this jar is special! Different source folder! return new Path(install.getHome()).append("scripts"); } else { if (GrailsVersion.V_2_0_0.compareTo(install.getVersion()) <= 0) { // >= 2.0.0 return the sources jar in the src folder return new Path(install.getHome()).append("src").append(file.getName().replace(".jar", "-sources.jar")); } else { // <= 1.3.7 return the grails/src/java path return new Path(install.getHome()).append("src").append("java"); } } } } return null; } /** * Returns configured javadoc attachment paths for a given jar resource path. * @param project the java project which preferences needs to be checked. * @param file the jar that needs a source attachment */ public static String getJavaDocLocation(IProject project, File file) { SpringCorePreferences prefs = SpringCorePreferences.getProjectPreferences(project, GrailsCoreActivator.PLUGIN_ID); return prefs.getString("javadoc.location-" + file.getName(), null); } /** * Returns configured source attachment paths for a given jar resource path. * @param project the java project which preferences needs to be checked. * @param file the jar that needs a source attachment */ public static IPath getSourceAttachmentFromUserPrefs(IProject project, File file) { SpringCorePreferences prefs = SpringCorePreferences.getProjectPreferences(project, GrailsCoreActivator.PLUGIN_ID); String value = prefs.getString("source.attachment-" + file.getName(), null); if (value != null) { return new Path(value); } return null; } /** * Two Grails class path containers are equal if all class path entries are the same AND all plugin descriptors are * also the same. If both are null, it also returns true. It return false in other cases. */ public static boolean areContainersEqual(GrailsClasspathContainer container1, GrailsClasspathContainer container2) { if (container1 == container2) { return true; } if (container1 != null && container2 != null) { Set<String> thisPluginDescriptors = container1.getPluginDescriptors(); Set<String> otherPluginDescriptors = container2.getPluginDescriptors(); if (!areDescriptorsEqual(thisPluginDescriptors, otherPluginDescriptors)) { return false; } return Arrays.deepEquals(container1.getClasspathEntries(), container2.getClasspathEntries()); } return false; } /** * Returns true if either both descriptor sets are null, both are empty, or both contain the exact same content. * Returns false otherwise. */ protected static boolean areDescriptorsEqual(Set<String> set1, Set<String> set2) { if (set1 == set2) { return true; } if (set1 != null && set2 != null && set1.size() == set2.size()) { for (String descriptor : set1) { if (!set2.contains(descriptor)) { return false; } } return true; } return false; } public static boolean isGrailsClasspathContainer(IClasspathEntry entry) { if (entry.getEntryKind()==IClasspathEntry.CPE_CONTAINER) { return entry.getPath().equals(CLASSPATH_CONTAINER_PATH); } return false; } public static boolean isVersionSynched(IProject project) { GrailsClasspathContainer container = GrailsClasspathUtils.getClasspathContainer(JavaCore.create(project)); if (container!=null) { GrailsVersion oldVersion = container.getGrailsVersion(); if (oldVersion!=null) { GrailsVersion newVersion = GrailsVersion.getGrailsVersion(project); return oldVersion.equals(newVersion); } return false; } //treat as version synched the project doesn't even seem like a real/valid grails project... so this will avoid doing refresh dependencies on it. // that's a good idea because it makes not much sense to refresh it anyway unless its a correctly setup grails project. return true; } private GrailsVersion getGrailsVersion() { return this.grailsVersion; } }