org.grails.ide.eclipse.commands.GrailsCommandUtils.java Source code

Java tutorial

Introduction

Here is the source code for org.grails.ide.eclipse.commands.GrailsCommandUtils.java

Source

/*******************************************************************************
 * 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
 * https://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);
    }
}