/******************************************************************************* * 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.commands; import java.io.File; import java.io.IOException; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Map; import java.util.Properties; import java.util.Set; import org.apache.commons.io.FileUtils; import org.eclipse.core.resources.ICommand; 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.IWorkspace; import org.eclipse.core.resources.IWorkspaceRunnable; import org.eclipse.core.resources.IncrementalProjectBuilder; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Path; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.launching.JavaRuntime; import org.grails.ide.eclipse.core.GrailsCoreActivator; import org.grails.ide.eclipse.core.internal.GrailsNature; import org.grails.ide.eclipse.core.internal.classpath.GrailsClasspathContainer; import org.grails.ide.eclipse.core.internal.classpath.GrailsClasspathUtils; import org.grails.ide.eclipse.core.internal.classpath.GrailsPluginVersion; import org.grails.ide.eclipse.core.internal.classpath.PerProjectDependencyDataCache; import org.grails.ide.eclipse.core.internal.classpath.SourceFolderJob; import org.grails.ide.eclipse.core.internal.plugins.GrailsCore; import org.grails.ide.eclipse.core.internal.plugins.PerProjectPluginCache; import org.grails.ide.eclipse.core.launch.SynchLaunch.ILaunchResult; import org.grails.ide.eclipse.core.model.GrailsBuildSettingsHelper; import org.grails.ide.eclipse.core.model.GrailsInstallManager; import org.grails.ide.eclipse.core.model.GrailsVersion; import org.grails.ide.eclipse.core.model.IGrailsInstall; 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.springsource.ide.eclipse.commons.frameworks.core.ExceptionUtil; /** * Utility class where we can place methods that provide a variety of Eclipse * related bookkeeping operations, such as refreshing resources, recomputing * classpath container etc. * <p> * These operations are not part of the Grails command itself, but often need to * be executed as post-processing step with Grails commands. * @author Kris De Volder * @author Nieraj Singh * @author Andrew Eisenberg */ public class GrailsCommandUtils { private static final String M2E_NATURE = "org.eclipse.m2e.core.maven2Nature"; /** * Defines the output folder that eclipsify project will configure a project with by default. */ public static final String DEFAULT_GRAILS_OUTPUT_FOLDER = "target-eclipse/classes"; //old value: //= "web-app/WEB-INF/classes"; public static boolean DEBUG = false; private static void debug(String msg) { if (DEBUG) { System.out.println(msg); } } /** * Newly created Grails projects (created by create-app / create-plugin) * have a number of issues with their setup (classpath, project natures * etc.). This method fixes those issues. * * @param grailsInstall * The grails install that should be stored in the configuration * of the project, can be null, if null the default Grails * install will be used. * @param isDefault * Configures whether this project uses a global default grails * install or uses a project specific Grails install. Is true, * the grailsInstall parameter will be ignored. * @param path * absolute location of the project (where the .project file is * located). If not specified, then an actual IProject must be * passed. * @param project * The project to configure. If no project is specified, the an * absolute path to the project location (where the .project file * is), must be specified * @throws CoreException */ private static IProject eclipsifyProject(IGrailsInstall grailsInstall, IPath projectAbsolutePath, IProject project) throws CoreException { if (grailsInstall == null) { grailsInstall = GrailsCoreActivator.getDefault() .getInstallManager().getDefaultGrailsInstall(); if (grailsInstall == null) { GrailsCoreActivator .log("Failed to create Grails project. No default grails version specified", null); return null; } } String grailsInstallName = grailsInstall.getName(); IPath projectDescPath = projectAbsolutePath; // The project has higher priority than the path argument. if (project != null) { projectDescPath = project.getLocation(); if (projectDescPath==null) { // Can be null, if project isn't created yet. projectDescPath = ResourcesPlugin.getWorkspace().getRoot().getLocation().append(project.getName()); } } if (projectDescPath == null) { GrailsCoreActivator .log("Failed to create Grails project. No path or project specified", null); return null; } projectDescPath = projectDescPath.append(".project"); IWorkspace workspace = ResourcesPlugin.getWorkspace(); IProjectDescription desc = workspace .loadProjectDescription(projectDescPath); if (desc != null) { if (project==null) { project = workspace.getRoot().getProject(desc.getName()); } // boolean addJavaNature = addNaturesAndBuilders(desc); if (!project.exists()) { project.create(desc, new NullProgressMonitor()); } project.open(0, new NullProgressMonitor()); project.setDescription(desc, new NullProgressMonitor()); // save selected grails install //GrailsInstallManager.setGrailsInstall(project, isDefault, grailsInstallName); GrailsClassPath entries = new GrailsClassPath(); IJavaProject javaProject = JavaCore.create(project); GrailsProject grailsProject = GrailsWorkspace.get().create(project); //Nowadays, we always create all classpath entries from scratch... //But to avoid breaking test //GrailsProjectVersionFixerTest.testCleanupLegacyLinkedSourceFolders() //We must ensure to cleanup the 'legacy' linked source folders from //before we changed this into using a single link to the plugins folder. SourceFolderJob.cleanupLegacyLinkedSourceFolders(SourceFolderJob.getGrailsSourceClasspathEntries(javaProject)); // Add output folder setDefaultOutputFolder(javaProject); // Add source entries to classpath final String[] sourcePaths = { "src/java", "src/groovy", "grails-app/conf", "grails-app/controllers", "grails-app/domain", "grails-app/services", "grails-app/taglib", "grails-app/utils", "test/integration", "test/unit" }; for (String srcPath : sourcePaths) { IFolder srcFolder = project.getFolder(srcPath); if (srcFolder.exists()) { entries.add(JavaCore.newSourceEntry(srcFolder.getFullPath())); } } //Add the Java libraries entries.add(JavaCore.newContainerEntry(Path.EMPTY.append(JavaRuntime.JRE_CONTAINER))); // Add the Grails classpath container entries.add(JavaCore.newContainerEntry( GrailsClasspathContainer.CLASSPATH_CONTAINER_PATH, null, null, false)); grailsProject.setClassPath(entries, new NullProgressMonitor()); // Make sure class path container and source folders are up-to-date try { refreshDependencies(javaProject, true); } catch (Exception e) { //Sometimes Grails throws exceptions because incomplete classpath and it //needs a second refresh before it gets the classpath right. refreshDependencies(javaProject, true); } javaProject.getProject().build(IncrementalProjectBuilder.CLEAN_BUILD, null); return project; } return null; } /** * (Re)sets a given grails project's output folder to the default. */ public static void setDefaultOutputFolder(IJavaProject javaProject) throws JavaModelException { IProject project = javaProject.getProject(); IFolder binDir = project.getFolder(DEFAULT_GRAILS_OUTPUT_FOLDER); IPath binPath = binDir.getFullPath(); javaProject.setOutputLocation(binPath, null); } /** * Adds natures and builders to a project descriptor. * @param desc * @return true if a Java nature was added, false if Java nature was already present. */ private static boolean addNaturesAndBuilders(IProjectDescription desc) { // prepare natures Set<String> natures = new LinkedHashSet<String>(); natures.add(GrailsNature.NATURE_ID); natures.add("org.eclipse.jdt.groovy.core.groovyNature"); for (String nature : desc.getNatureIds()) { if (!nature.contains("groovy")) { natures.add(nature); } } boolean addJavaNature = !natures.contains(JavaCore.NATURE_ID); if (addJavaNature) { natures.add(JavaCore.NATURE_ID); } natures.remove(GrailsNature.OLD_NATURE_ID); desc.setNatureIds(natures.toArray(new String[natures .size()])); // prepare builder Set<ICommand> builders = new LinkedHashSet<ICommand>(); for (ICommand builder : desc.getBuildSpec()) { if (!builder.getBuilderName().contains("groovy")) { builders.add(builder); } } desc.setBuildSpec(builders.toArray(new ICommand[builders.size()])); return addJavaNature; } // private static void setGrailsInstall(IProject project, IGrailsInstall grailsInstall) { // GrailsInstallManager.setGrailsInstall(project, grailsInstall.isDefault() && GrailsInstallManager.inheritsDefaultInstall(project), grailsInstall.getName()); // } /** * Newly created Grails projects (created by create-app / create-plugin) * have a number of issues with their setup (classpath, project natures * etc.). This method fixes those issues. * * @param grailsInstall * The grails install that should be stored in the configuration * of the project, can be null, if null the default Grails * install will be used. * @param isDefault * Configures whether this project uses a global default grails * install or uses a project specific Grails install. Is true, * the grailsInstall parameter will be ignored. * @param project * The project to configure. Cannot be null * @throws CoreException */ public static IProject eclipsifyProject(IGrailsInstall grailsInstall, IProject project) throws CoreException { return eclipsifyProject(grailsInstall, null, project); } public static IProject eclipsifyProject(IGrailsInstall install, IPath projectPath) throws CoreException { return eclipsifyProject(install,projectPath, null); } /** * Recompute the Grails class path container. Essentially this performs the * same action as the "Refresh Dependencies" Grails menu command. * <p> * This is a potentially long running process and so it should not be called * directly from the UI thread. When running in the UI thread you should * wrap calls to this (and other work you are possibly doing alongside with * this in some type of background Job.). */ public static void refreshDependencies(final IJavaProject javaProject, boolean showOutput) throws CoreException { debug("Refreshing dependencies for "+javaProject.getElementName()+" ..."); // This job is a no-op for maven projects since maven handles the source folders if (isMavenProject(javaProject)) { // don't do refresh dependencies on maven projects. This is handled by project configurator debug("Not refreshing dependencies because this is a maven project."); return; } GroovyCompilerVersionCheck.check(javaProject); deleteOutOfSynchPlugins(javaProject.getProject()); // Create the external process part and launch it synchronously... GrailsCommand refreshFileCmd = GrailsCommandFactory.refreshDependencyFile(javaProject.getProject()); refreshFileCmd.setShowOutput(showOutput); ILaunchResult result = refreshFileCmd.synchExec(); debug(result.toString()); //TODO: KDV: (depend) if we do it right, we should be able to remove the call to the refreshFileCmd below. However, this // assumes that // a) we ensure that any command that may change the state of the dependencies also forces // the regeneration of the data file as part of its own execution. (Currently this isn't the case) // b) RefreshGrailsDependencyActionDelegate also forces the data file to be regenerated somehow // Making this change is desirable (executing the command below takes a long time). // Making this change is difficult at the moment because many commands do not go via the GrailsCommand // class. In particular, commands executed via the command prompt still directly use the old GrailsLaunchConfigurationDelegate, // ILaunchConfiguration configuration = GrailsDependencyLaunchConfigurationDelegate // .getLaunchConfiguration(javaProject.getProject()); // SynchLaunch sl = new SynchLaunch(configuration, GrailsCoreActivator.getDefault().getGrailsCommandTimeOut()); // sl.setShowOutput(showOutput); // sl.synchExec(); // ensure that this operation runs without causing multiple builds IWorkspace workspace = ResourcesPlugin.getWorkspace(); workspace.run(new IWorkspaceRunnable() { public void run(IProgressMonitor monitor) throws CoreException { // Grails "compile" command may have changed resources...? // TODO: KDV: (depend) find out why this refresh is necessary. See STS-1263. // Note: if this line is removed, it *will* break STS-1270. We should revisit // where calls are being made to refresh the resource tree. Suspect we may doing this more than // once in some cases. javaProject.getProject().refreshLocal(IResource.DEPTH_INFINITE, monitor); // Now that we got here the data file should be available and // we can ask GrailsClasspathContainer to refresh its dependencies. GrailsClasspathContainer container = GrailsClasspathUtils.getClasspathContainer(javaProject); // reparse classpath entries from dependencies file on next request if (container != null) { container.invalidate(); } // ensure that the dependency and plugin data is forgotten GrailsCore.get().connect(javaProject.getProject(), PerProjectDependencyDataCache.class).refreshData(); GrailsCore.get().connect(javaProject.getProject(), PerProjectPluginCache.class).refreshDependencyCache(); // recompute source folders now SourceFolderJob updateSourceFolders = new SourceFolderJob(javaProject); updateSourceFolders.refreshSourceFolders(new NullProgressMonitor()); // This will force the JDT to re-resolve the CP, even if only the "contents" of class path container changed see STS-1347 javaProject.setRawClasspath(javaProject.getRawClasspath(), monitor); } }, new NullProgressMonitor()); debug("Refreshing dependencies for "+javaProject.getElementName()+" DONE"); } protected static boolean isMavenProject(IJavaProject javaProject) throws CoreException { try { return javaProject.getProject().hasNature(M2E_NATURE); } catch (CoreException e) { GrailsCoreActivator.log(e); return false; } } /** * Grails 1.3.5 and 1.3.6 won't update plugin versions during grails compile when the update is * a "downgrade" to an older version. To remedy this, a workaround is to delete the * plugins folders in the .grails folder that are causing problems. * <p> * See STS-1263 */ public static void deleteOutOfSynchPlugins(IProject project) { IGrailsInstall install = GrailsCoreActivator.getDefault().getInstallManager().getGrailsInstall(project); if (install == null || GrailsVersion.UNKNOWN.equals(install.getVersion())) { GrailsVersion needsVersion = GrailsVersion.getGrailsVersion(project); throw new IllegalArgumentException("Could not find a grails install (needed version = "+ needsVersion + ") for '"+project.getName()+"'. " + "Please configure a Grails "+needsVersion+" install from the Grails preferences page."); } if (true/*GrailsVersion.V_1_3_5.compareTo(grailsVersion)<=0*/) { //This workaround is only required for grails 1.3.5 (until the bug that requires it is fixed) //The workaround should not be harmful even if the bug it addresses is fixed. PerProjectPluginCache pluginCache = GrailsCore.get().connect(project, PerProjectPluginCache.class); PerProjectDependencyDataCache depDataCache = GrailsCore.get().connect(project, PerProjectDependencyDataCache.class); Map<String, GrailsPluginVersion> pluginMap = pluginCache.getPluginDataMap(); Properties props = GrailsBuildSettingsHelper.getApplicationProperties(project); for (Map.Entry<String, GrailsPluginVersion> entry : pluginMap.entrySet()) { String pluginXml = entry.getKey(); GrailsPluginVersion grailsPluginVersion = entry.getValue(); //Now we need to decide if this plugin should be deleted from the .grails folder String pluginName = grailsPluginVersion.getName(); String pluginInstalledVersion = grailsPluginVersion.getVersion(); String propVersion = (String) props.get("plugins."+pluginName); debug("Current plugin: "+pluginName+" version: "+pluginInstalledVersion + " application.properties = "+propVersion); if (propVersion!=null) { if (!propVersion.equals(pluginInstalledVersion)) { //Plugin exists in both the .grails folder and application.properties //and versions are out of synch ==> Delete it! // Complication: if a user adds the inplace plugin to application.properties the code below may end // up deleting the inplace plugin (which is extremely bad, since that is the user's code!). // This scenario is unlikely since inplace plugins are not usually added to application.properties // (since this doesn't even work), but we check for it anyway (since deleting the user's code is extremely // undesirable). boolean inPluginsFolder = pluginXml.startsWith(depDataCache.getData().getPluginsDirectory()); debug("Plugin inPluginsFolder = "+inPluginsFolder); if (inPluginsFolder) { //One of the above checks would suffice, but better be safe than sorry! debug("Should delete this plugin: "+pluginXml); File pluginXmlFile = new File(pluginXml); File pluginDir = pluginXmlFile.getParentFile(); try { FileUtils.deleteDirectory(pluginDir); debug("Deleted"); } catch (IOException e) { GrailsCoreActivator.log(e); } } } } } } } /** * Execute a "grails upgrade" command to upgrade a grails project to the given grails install. * @throws CoreException */ public static void upgradeProject(IProject project, IGrailsInstall install) throws CoreException { debug("upgrade "+project+" ..."); //setGrailsInstall(project, install); ensureNaturesAndBuilders(project); // This is needed to avoid upgrade from crashing in the 'old style' // executor when imported project is missing Java nature. CoreException error = null; try { ILaunchResult result = GrailsCommandFactory.upgrade(project, install).synchExec(); debug(""+result); debug("upgrade "+project+" DONE"); } catch (CoreException e) { debug("upgrade "+project+" FAILED"); error = e; } //Even if above command exec had some error, we can try to proceed... try { eclipsifyProject(install, project); } catch (CoreException e) { if (error==null) { error = e; } } if (error!=null) { throw error; } } public static void ensureNaturesAndBuilders(IProject project) throws CoreException { IProjectDescription desc = project.getDescription(); addNaturesAndBuilders(desc); project.setDescription(desc, new NullProgressMonitor()); } /** * Refreshes the dependencies of a given project and all projects that depend on it. * Note: this is not truely transitive, it only looks one level deep in the dependencies, * assuming that transitive dependencies are already added as dependencies to a project. */ public static void transitiveRefreshDependencies(GrailsProject gp, boolean showOutput) throws CoreException { if (!gp.isPlugin()) { //Shortcut: the transitive bit only matters for plugin projects. refreshDependencies(gp.getJavaProject(), showOutput); } else { transitiveRefreshDependencies(gp, showOutput, new HashSet<GrailsProject>()); } } /** * Helper method to perform transitive refreshing. An 'already' refreshed Set is passed around and used * to avoid refreshing the same project multiple times. */ private static void transitiveRefreshDependencies(GrailsProject gp, boolean showOutput, HashSet<GrailsProject> alreadyRefreshed) throws CoreException { if (!alreadyRefreshed.contains(gp)) { refreshDependencies(gp, showOutput); alreadyRefreshed.add(gp); Set<GrailsProject> needsRefreshing = gp.getProjectsDependingOn(); for (GrailsProject dependor : needsRefreshing) { transitiveRefreshDependencies(dependor, showOutput, alreadyRefreshed); } } } private static void refreshDependencies(GrailsProject gp, boolean showOutput) throws CoreException { refreshDependencies(gp.getJavaProject(), showOutput); } public static void eclipsifyProject(IProject project) throws CoreException { IGrailsInstall install = GrailsCoreActivator.getDefault().getInstallManager().getGrailsInstall(project); if (install==null) { GrailsVersion version = GrailsVersion.getGrailsVersion(project); throw ExceptionUtil.coreException("No matching Grails Install (required "+version+") for project '"+project.getName()+"'"); } eclipsifyProject(install, project); } }