/******************************************************************************* * Copyright (c) 2006 RadRails.org and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Common Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html *******************************************************************************/ package com.aptana.ruby.internal.rake; import java.io.BufferedReader; import java.io.IOException; import java.io.StringReader; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; 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.Platform; import org.eclipse.core.runtime.Status; import org.eclipse.debug.core.DebugPlugin; import org.eclipse.debug.core.ILaunchConfiguration; import org.eclipse.debug.core.ILaunchConfigurationType; import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; import org.eclipse.debug.core.ILaunchManager; import org.eclipse.debug.ui.DebugUITools; import org.eclipse.debug.ui.IDebugUIConstants; import com.aptana.core.ShellExecutable; import com.aptana.core.logging.IdeLog; import com.aptana.core.util.ProcessUtil; import com.aptana.core.util.StringUtil; import com.aptana.ruby.debug.core.launching.IRubyLaunchConfigurationConstants; import com.aptana.ruby.launching.RubyLaunchingPlugin; import com.aptana.ruby.rake.IRakeHelper; import com.aptana.ruby.rake.RakePlugin; /** * @author mkent * @author cwilliams */ public class RakeTasksHelper implements IRakeHelper { /** * If we fail to find a ruby executable on PATH, use this command as last ditch try at running something under ruby * intrerpreter. */ private static final String RUBY_EXE_NAME = RubyLaunchingPlugin.RUBY; /** * Command line switch used to have rake list the available tasks that can be run. */ private static final String TASK_LIST_SWITCH = "--tasks"; //$NON-NLS-1$ /** * Regexp used to extract the task name and descriptions from the output of rake with the {@link #TASK_LIST_SWITCH} */ private static final Pattern RAKE_TASK_PATTERN = Pattern.compile("^rake\\s+([\\w:]+)\\s+#\\s+(.+)$"); //$NON-NLS-1$ /** * Special param used by rake to modify the ENV used when launching. */ private static final String ENV_MODIFYING_PARAM = "RAILS_ENV"; //$NON-NLS-1$ /** * Cache the task list, since rake is slow in listing tasks (~1.8s for me on Ruby 1.9) */ private Map<IProject, Map<String, String>> fCachedTasks = new HashMap<IProject, Map<String, String>>(); private IStatus runRakeInBackground(IProject project, IProgressMonitor monitor, String... arguments) { if (monitor == null) { monitor = new NullProgressMonitor(); } if (monitor.isCanceled()) { return Status.CANCEL_STATUS; } IPath wd = getWorkingDirectory(project); IPath rubyExe = RubyLaunchingPlugin.rubyExecutablePath(wd); Map<String, String> env; if (!Platform.OS_WIN32.equals(Platform.getOS())) { env = ShellExecutable.getEnvironment(wd); } else { env = new HashMap<String, String>(); } env = modifyEnv(env, arguments); List<String> args = new ArrayList<String>(); args.add(RakePlugin.getDefault().getRakePath(project)); // TODO Enforce at least one argument minimum (task name)? if (arguments != null) { for (String param : arguments) { args.add(param); } } return ProcessUtil.runInBackground((rubyExe == null) ? RUBY_EXE_NAME : rubyExe.toOSString(), wd, env, args.toArray(new String[args.size()])); } private IPath getWorkingDirectory(IProject project) { if (project == null) { return null; } try { RakeFileFinder finder = new RakeFileFinder(); project.accept(finder, IResource.NONE); IPath workingDir = finder.getWorkingDirectory(); if (workingDir != null) { return project.getLocation().append(workingDir); } if (IdeLog.isWarningEnabled(RakePlugin.getDefault(), null)) { IdeLog.logWarning(RakePlugin.getDefault(), "Failed to find parent of Rakefile to use as working dir for project: " + project.getName()); //$NON-NLS-1$ } } catch (CoreException e) { RakePlugin.log(e); } return project.getLocation(); } /** * Gets the rake tasks for the passed in project * * @param project * The IProject to gather rake tasks for * @return a Map of rake task names to their descriptions */ public Map<String, String> getTasks(IProject project, IProgressMonitor monitor) { return getTasks(project, false, monitor); } /** * Gets the rake tasks for the passed in project * * @param project * The IProject to gather rake tasks for * @param force * Whether or not to force a refresh (don't grab cached value) * @return a Map of rake task names to their descriptions */ public Map<String, String> getTasks(IProject project, boolean force, IProgressMonitor monitor) { if (!force && fCachedTasks.containsKey(project)) { return Collections.unmodifiableMap(fCachedTasks.get(project)); } if (monitor != null && monitor.isCanceled()) { return Collections.emptyMap(); } BufferedReader bufReader = null; try { bufReader = new BufferedReader(new StringReader(getTasksText(project))); String line = null; Map<String, String> tasks = new HashMap<String, String>(); while ((line = bufReader.readLine()) != null) // $codepro.audit.disable assignmentInCondition { Matcher mat = RAKE_TASK_PATTERN.matcher(line); if (mat.matches()) { tasks.put(mat.group(1), mat.group(2)); } } fCachedTasks.put(project, tasks); } catch (IOException e) { RakePlugin.log("Error parsing rake tasks", e); //$NON-NLS-1$ return Collections.emptyMap(); } finally { if (bufReader != null) { try { bufReader.close(); } catch (final IOException e) // $codepro.audit.disable emptyCatchClause { // ignore } } } return fCachedTasks.get(project); } private String getTasksText(IProject project) { IStatus status = runRakeInBackground(project, new NullProgressMonitor(), TASK_LIST_SWITCH); if (status.isOK()) { return status.getMessage(); } return StringUtil.EMPTY; } public IStatus runRake(IProject project, IProgressMonitor monitor, String... arguments) { try { ILaunchConfiguration config = findOrCreateLaunchConfiguration(project, arguments); if (config != null) { // FIXME Must call this in the UI thread! DebugUITools.launch(config, ILaunchManager.RUN_MODE); } else { return new Status(IStatus.ERROR, RakePlugin.PLUGIN_ID, MessageFormat.format( Messages.RakeTasksHelper_LaunchGenerationFailed, project, arguments)); } } catch (CoreException e) { RakePlugin.log(e); return e.getStatus(); } return Status.OK_STATUS; } private Map<String, String> modifyEnv(Map<String, String> env, String... arguments) { Map<String, String> modified = new HashMap<String, String>(env); if (arguments != null) { for (String param : arguments) { if (param.contains(ENV_MODIFYING_PARAM + "=")) //$NON-NLS-1$ { String value = param.substring(param.indexOf(ENV_MODIFYING_PARAM + "=") + 10); //$NON-NLS-1$ if (value.indexOf(' ') != -1) { value = value.substring(0, value.indexOf(' ')); } modified.put(ENV_MODIFYING_PARAM, value); } } } return modified; } @SuppressWarnings("rawtypes") private ILaunchConfiguration findOrCreateLaunchConfiguration(IProject project, String... arguments) throws CoreException { String rakeScriptPath = RakePlugin.getDefault().getRakePath(project); IPath wd = getWorkingDirectory(project); Map<String, String> env = modifyEnv(new HashMap<String, String>(), arguments); StringBuilder args = new StringBuilder(); if (arguments != null && arguments.length > 0) { for (String argument : arguments) { args.append(argument).append(' '); } // delete last space args.deleteCharAt(args.length() - 1); } ILaunchConfigurationType configType = getRubyLaunchConfigType(); ILaunchConfiguration[] configs = getLaunchManager().getLaunchConfigurations(configType); List<ILaunchConfiguration> candidateConfigs = new ArrayList<ILaunchConfiguration>(configs.length); for (ILaunchConfiguration config : configs) { boolean absoluteFilenamesMatch = config.getAttribute(IRubyLaunchConfigurationConstants.ATTR_FILE_NAME, StringUtil.EMPTY).equals(rakeScriptPath); if (!absoluteFilenamesMatch) { continue; } boolean argsMatch = config.getAttribute(IRubyLaunchConfigurationConstants.ATTR_PROGRAM_ARGUMENTS, StringUtil.EMPTY).equals(args.toString()); if (!argsMatch) { continue; } boolean envMatches = env.equals(config.getAttribute(ILaunchManager.ATTR_ENVIRONMENT_VARIABLES, (Map) null)); if (!envMatches) { continue; } candidateConfigs.add(config); } switch (candidateConfigs.size()) { case 0: return createConfiguration(project, wd, rakeScriptPath, args.toString(), env); case 1: return candidateConfigs.get(0); default: Status status = new Status(Status.WARNING, RakePlugin.PLUGIN_ID, 0, "Multiple configurations match", null); //$NON-NLS-1$ throw new CoreException(status); } } private ILaunchConfiguration createConfiguration(IProject project, IPath workingDir, String rubyFile, String args, Map<String, String> env) throws CoreException { // TODO Combine this into some utility method somewhere? ILaunchConfigurationType configType = getRubyLaunchConfigType(); ILaunchConfigurationWorkingCopy wc = configType.newInstance(null, getLaunchManager() .generateLaunchConfigurationName(MessageFormat.format("{0} rake {1}", project.getName(), args))); //$NON-NLS-1$ wc.setAttribute(IRubyLaunchConfigurationConstants.ATTR_FILE_NAME, rubyFile); wc.setAttribute(IRubyLaunchConfigurationConstants.ATTR_WORKING_DIRECTORY, workingDir.toOSString()); wc.setAttribute(IRubyLaunchConfigurationConstants.ATTR_PROGRAM_ARGUMENTS, args); wc.setAttribute(ILaunchConfiguration.ATTR_SOURCE_LOCATOR_ID, IRubyLaunchConfigurationConstants.ID_RUBY_SOURCE_LOCATOR); wc.setAttribute(IDebugUIConstants.ATTR_PRIVATE, true); wc.setAttribute(ILaunchManager.ATTR_APPEND_ENVIRONMENT_VARIABLES, true); wc.setAttribute(ILaunchManager.ATTR_ENVIRONMENT_VARIABLES, env); return wc.doSave(); } protected ILaunchConfigurationType getRubyLaunchConfigType() { return getLaunchManager().getLaunchConfigurationType(IRubyLaunchConfigurationConstants.ID_RUBY_APPLICATION); } protected ILaunchManager getLaunchManager() { return DebugPlugin.getDefault().getLaunchManager(); } }