/******************************************************************************* * 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.HashMap; import java.util.Map; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.grails.ide.eclipse.core.GrailsCoreActivator; import org.grails.ide.eclipse.core.internal.classpath.GrailsClasspathUtils; import org.grails.ide.eclipse.core.internal.classpath.PerProjectDependencyDataCache; import org.grails.ide.eclipse.core.internal.plugins.GrailsCore; import org.grails.ide.eclipse.core.launch.SynchLaunch.ILaunchResult; import org.grails.ide.eclipse.core.launch.SynchLaunch.LaunchResult; import org.grails.ide.eclipse.core.model.GrailsBuildSettingsHelper; 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.grails.ide.eclipse.runtime.shared.SharedLaunchConstants; /** * Class to help executing Grails commands. Contains various settings needed to * execute a given grails command and does whatever is necessary to execute it. * It provides a mechanism to execute the command synchronously. * @author Kris De Volder * @author Nieraj Singh * @author Andrew Eisenberg */ public class GrailsCommand { /** * Timeout value used for grails commands if the preference is not set in the * preferences page. */ public static final int DEFAULT_TIMEOUT = 180000; //bumped for Grails 2.3 // resolving dependencies some large stuff downloads // and may take a while before it prints more progress info private String command; private IProject project; private String path; private IGrailsInstall install; /** * Optional attribute: a class that should be attached to the external process as a * grails build listener. */ private String buildListener; /** * Optional attribute: contains key, value pairs that will be passed on to the * external grails process in such a way so that it can access them via * {@link System}.getProperty(). */ private Map<String, String> systemProperties; /** * Output from this command shows in the console view? */ private boolean isShowOutput = true; /** * Method is now package private. Please use GrailsCommandFactory to create commands. */ GrailsCommand(IProject project, String command) { this(null, project, command); } /** * This constructor is package private and shouldn't be called by clients. However it should * be called by all other constructors because it contains some work that should * always be done regardless of the parameters. */ GrailsCommand(IGrailsInstall install, IProject project, String command) { this(install, project, command, false); } /** * When we no longer require the 'dirty' flag (all code is abiding by the intended * api usage and doesn't pass in command strings that have spaces in them). This constructor * should be removed so the assert is always executed. * @deprecated */ private GrailsCommand(IGrailsInstall install, IProject project, String command, boolean dirty) { this.systemProperties = GrailsCoreActivator.getDefault().getLaunchSystemProperties(); if (!dirty) { Assert.isLegal(command.indexOf(' ')<0, "Initial command String should only contain the name of a command. " + "Add extra arguments with the addArgument method to ensure that proper " + "escape sequences are used for ' ' inside of arguments."); } this.install = install; this.project = project; this.command = command; } /** * Launch a command, outside the scope of a particular project. This command * will be launched using the Default grails install and the current * directory set to the workspace root. * <p> * Suitable for running commands like "create-app" and "create-plugin" which * can not be executed in the context of a project, because their purpose is * the creation of the project. * <p> * Method is now package private. Please use GrailsCommandFactory to create commands. */ GrailsCommand(String command) { this(null, null, command); } /** * Launch a command, outside the scope of a particular project. This command * will be launched using the provided grails install. The current directory * will be set to the workspace root. * <p> * Suitable for running commands like "create-app" and "create-plugin" which * can not be executed in the context of a project, because their purpose is * the creation of the project. * <p> * Method is now package private. Please use GrailsCommandFactory to create commands. */ GrailsCommand(IGrailsInstall install, String command) { this(install, null, command); } GrailsCommand(IProject proj) { this(null, proj, ""); } /** * This constructor is deprecated. It allows 'old style' code that creates a command * string by simply splicing all the bits together with '+'. Such code is considered 'dirty' * because it provides little guarantee that spaces inside of command * argument will not cause trouble. Such code should be restructured to * us GrailsCommand.addArgument instead. * * @deprecated */ GrailsCommand(IProject project, String command, boolean dirty) { this(null, project, command, dirty); } /** * Execute the command "synchronously" i.e. block the current thread until * the command finishes. * * @return An object containing information about the command's result, such * as the output/error stream contents. * @throws CoreException * if something went wrong. */ public ILaunchResult synchExec() throws CoreException { if (JDKCheck.check(getProject())) { return GrailsExecutor.getInstance(getGrailsVersion()).synchExec(this); } else { throw new LaunchResult(-1) { public IStatus getStatus() { return new Status(IStatus.CANCEL, GrailsCoreActivator.PLUGIN_ID, "User canceled command"); } }.getCoreException(); } } public GrailsVersion getGrailsVersion() { return getGrailsInstall().getVersion(); } public void runPostOp() { if (this.command.contains("clean")) { //A fix for https://issuetracker.springsource.com/browse/STS-2941 // Ensure the plugin-classes directory which may be deleted by grails clean // exists at least as an empty dir to avoid error from grails class path container. PerProjectDependencyDataCache info = GrailsCore.get().connect(project, PerProjectDependencyDataCache.class); if (info!=null) { DependencyData data = info.getData(); if (data!=null) { File pluginClasses = data.getPluginClassesDirectoryFile(); if (!pluginClasses.exists()) { pluginClasses.mkdirs(); } } } } } /** * Get timeout value that is used to determine if the external process executing this * command is "stuck". A process is deemed to be stuck if it does not return new * input within the timeout. Each time new input is seen the timeout is reset. * * @return Timeout value in milliseconds. */ public int getGrailsCommandTimeOut() { return GrailsCoreActivator.getDefault().getGrailsCommandTimeOut(); } public IGrailsInstall getGrailsInstall() { if (install != null) { return install; } else if (project != null) { return GrailsCoreActivator.getDefault().getInstallManager() .getGrailsInstall(project); } else { return GrailsCoreActivator.getDefault().getInstallManager() .getDefaultGrailsInstall(); } } @Override public String toString() { if (project != null) { return "GrailsCommand(" + project + "> " + command + ")"; } else { return "GrailsCommand(" + command + ")"; } } /** * Calling this method will enable the creation of a new {@link DependencyData} file in the * default location for the associated project. The creating of the file will be triggered * near the end of the command's execution. Using this is more efficient than creating the * dependency data in a new grails process (since we would have to pay the cost of * booting grails twice. * <p> * Note: this only forces a refresh of the data *file* but doesn't force the data file to actually * be read by eclipse. */ public void enableRefreshDependencyFile() { attachBuildListener(SharedLaunchConstants.DependencyExtractingBuildListener_CLASS); setSystemProperty(SharedLaunchConstants.DEPENDENCY_FILE_NAME_PROP, GrailsClasspathUtils.getDependencyDescriptorName(project)); } ///////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * Attach a given class as a "build listener" to the resulting grails process. Note that the process runs in * an other JVM and the build listener is classloaded by it, so there is no way to directly share state with * this build listener! * <p> * When a build listener is attached to a command, a reference the {@link GrailsCoreActivator} plugin will be * added to the launch's classpath. It is therefore assumed that the build listener class, is declared in that * plugin. * <p> * Any information the build listener wants to communicate back should be passed in some creative way, like * saving this info into a file or sending it back over a socket. * <p> * For an example on how to use this, see GrailsCommandTest. */ public void attachBuildListener(String klass) { Assert.isLegal(buildListener==null || buildListener==klass, "Attaching multiple build listeners is not (yet) supported"); this.buildListener = klass; } /** * Any key-value pairs registered by calling this method will be passed on to the external process, * in such a way that they can be accessed by calling System.getProperty from the external process. */ public void setSystemProperty(String key, String value) { if (systemProperties==null) { systemProperties = new HashMap<String, String>(); } systemProperties.put(key, value); } /** * If this is set to 'true' it will cause the command's output to be shown in the console view. * If false, the launch won't be registered with the UI and output won't be shown in the console view. * <p> * The default value for this property is 'true'. */ public void setShowOutput(boolean showOutput) { this.isShowOutput = showOutput; } /** * Sets the path where the command is to execute. If this is not set then some default path * will be used (if project is set the project determines the path, if not, the OS's current * directory is used implicitly). * * @param path */ public void setPath(String path) { this.path = path; } public String getBuildListener() { return buildListener; } public Map<String, String> getSystemProperties() { return systemProperties; } public IProject getProject() { return project; } public String getPath() throws IOException { if (path==null) { path = GrailsBuildSettingsHelper.getBaseDir(project); if (path==null) { path = ResourcesPlugin.getWorkspace().getRoot().getLocation().toFile().getCanonicalPath(); } } return path; } public String getCommand() { return command; } public boolean isShowOutput() { return isShowOutput; } public File getDependencyFile() { String file = getSystemProperty(SharedLaunchConstants.DEPENDENCY_FILE_NAME_PROP); if (file!=null) { return new File(file); } return null; } private String getSystemProperty(String propName) { if (systemProperties!=null) { return systemProperties.get(propName); } return null; } /** * Append an argument to the command string. This method escapes spaces to avoid the argument * being split into multiple arguments. * <p> * @return The receiver object to allow easy chaining of 'addArgument' calls. */ public GrailsCommand addArgument(String arg) { if (arg!=null) { if (!command.equals("")) { command = command + " "; } command = command+escapeArgument(arg); } return this; } private String escapeArgument(String argument) { Assert.isLegal(!(argument.contains("'") && argument.contains("\"")), "Can\'t handle single and double" + " quotes in same argument"); if (argument.contains("\"")) { return '\'' + argument + '\''; } else if (argument.contains("\'") || argument.contains(" ")) { return '\"' + argument + '\"'; } else { return argument; } } /** * This is a 'backdoor' to call the GrailsCommand constructor directly instead of via * a 'GrailsCommandFactory' method. This is only intended for unit testing * purposes. Normal clients should use GrailsCommandFactory to create some specific * type of command. */ public static GrailsCommand forTest(IProject project, String commandName) { return new GrailsCommand(project, commandName); } /** * This is a 'backdoor' to call the GrailsCommand constructor directly instead of via * a 'GrailsCommandFactory' method. This is only intended for unit testing * purposes. Normal clients should use GrailsCommandFactory to create some specific * type of command. */ public static GrailsCommand forTest(String commandName) { return new GrailsCommand(commandName); } }