/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2010 Oracle and/or its affiliates. All rights reserved.
*
* Oracle and Java are registered trademarks of Oracle and/or its affiliates.
* Other names may be trademarks of their respective owners.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common
* Development and Distribution License("CDDL") (collectively, the
* "License"). You may not use this file except in compliance with the
* License. You can obtain a copy of the License at
* http://www.netbeans.org/cddl-gplv2.html
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
* specific language governing permissions and limitations under the
* License. When distributing the software, include this License Header
* Notice in each file and include the License file at
* nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the GPL Version 2 section of the License file that
* accompanied this code. If applicable, add the following below the
* License Header, with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*
* If you wish your version of this file to be governed by only the CDDL
* or only the GPL Version 2, indicate your decision by adding
* "[Contributor] elects to include this software in this distribution
* under the [CDDL or GPL Version 2] license." If you do not indicate a
* single choice of license, a recipient has the option to distribute
* your version of this file under either the CDDL, the GPL Version 2 or
* to extend the choice of license to its licensees as provided above.
* However, if you add GPL Version 2 code and therefore, elected the GPL
* Version 2 license, then the option applies only if the new code is
* made subject to such option by the copyright holder.
*
* Contributor(s):
*
* Portions Copyrighted 2008 Sun Microsystems, Inc.
*/
package org.netbeans.modules.ruby.rubyproject.rake;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Future;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectInformation;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.api.ruby.platform.RubyPlatform;
import org.netbeans.api.extexecution.ExecutionService;
import org.netbeans.api.extexecution.print.ConvertedLine;
import org.netbeans.api.extexecution.print.LineConvertor;
import org.netbeans.modules.ruby.platform.Util;
import org.netbeans.modules.ruby.platform.execution.RubyExecutionDescriptor;
import org.netbeans.modules.ruby.platform.execution.RubyProcessCreator;
import org.netbeans.modules.ruby.rubyproject.RubyFileLocator;
import org.netbeans.modules.ruby.rubyproject.SharedRubyProjectProperties;
import org.netbeans.modules.ruby.rubyproject.spi.RakeTaskCustomizer;
import org.netbeans.modules.ruby.rubyproject.ui.customizer.RubyProjectProperties;
import org.netbeans.modules.ruby.spi.project.support.rake.PropertyEvaluator;
import org.openide.LifecycleManager;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.Parameters;
import org.openide.util.Utilities;
import org.openide.windows.OutputEvent;
import org.openide.windows.OutputListener;
import org.netbeans.modules.ruby.codecoverage.RubyCoverageProvider;
import org.netbeans.modules.ruby.rubyproject.RubyBaseProject;
/**
* Provides Rake running infrastructure.
*/
public final class RakeRunner {
private final RubyBaseProject project;
private boolean showWarnings;
private boolean debug;
private FileObject rakeFile;
private RubyFileLocator fileLocator;
private File pwd;
private String displayName;
private final List<String> parameters = new ArrayList<String>();
public RakeRunner(final RubyBaseProject project) {
this.project = project;
}
static void runTask(final RubyBaseProject project, final RakeTask task,
final String taskParams, final boolean debug) {
RakeRunner runner = new RakeRunner(project);
runner.showWarnings(true);
if (taskParams != null) {
runner.setParameters(Utilities.parseParameters(taskParams));
}
runner.setDebug(debug);
runner.run(task);
}
public void setRakeFile(final FileObject rakeFile) {
this.rakeFile = rakeFile;
}
/**
* @param warn if true, produce popups if {@link RubyPlatform} is not ready
* for running Rake.
*/
public void showWarnings(boolean showWarnings) {
this.showWarnings = showWarnings;
}
public void setDebug(boolean debug) {
this.debug = debug;
}
/**
* @param fileLocator the file locator to be used to resolve output
* hyperlinks
*/
public void setFileLocator(final RubyFileLocator fileLocator) {
this.fileLocator = fileLocator;
}
/**
* @param displayName the displayname to be shown in the output window
*/
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
/**
* @param pwd If you specify the rake file, you can pass null as the
* directory, and the directory containing the rakeFile will be used,
* otherwise it specifies the dir to run rake in
*/
public void setPWD(File pwd) {
this.pwd = pwd;
}
/**
* Sets the task parameters for <strong>all</strong> the tasks that will
* be run. These will be added after the task name but before any parameters
* that are set to <code>RakeTask</code>s that are being run.
*
* @param parameters the parameters to set; must not be null.
*/
public void setParameters(final String... parameters) {
Parameters.notNull("parameters", parameters);
for (String each : parameters) {
this.parameters.add(each);
}
}
/**
* Runs the tasks specifed by the given <code>taskNames</code>.
*
* @param taskNames the names of the tasks to run.
*/
public List<Future<Integer>> run(String... taskNames) {
if (taskNames.length == 0) {
taskNames = new String[]{"default"}; // NOI18N
}
if (!RubyPlatform.hasValidRake(project, showWarnings)) {
return null;
}
RakeTask[] rakeTasks = new RakeTask[taskNames.length];
for (int i = 0; i < taskNames.length; i++) {
RakeTask rakeTask = RakeSupport.getRakeTask(project, taskNames[i]);
if (rakeTask == null) {
if (showWarnings) {
Util.notifyLocalizedInfo(RakeRunner.class, "RakeRunner.task.does.not.exist", taskNames[i]); // NOI18N
}
return null; // run only when all tasks are available
}
rakeTasks[i] = rakeTask;
}
return run(rakeTasks);
}
private List<Future<Integer>> run(final RakeTask... tasks) {
assert tasks.length > 0 : "must pass at least one task";
if (!RubyPlatform.hasValidRake(project, showWarnings)) {
return null;
}
// Save all files first
LifecycleManager.getDefault().saveAll();
// EMPTY CONTEXT??
if (fileLocator == null) {
fileLocator = new RubyFileLocator(Lookup.EMPTY, project);
}
if (rakeFile == null) {
rakeFile = RakeSupport.findRakeFile(project);
}
if (rakeFile == null) {
pwd = FileUtil.toFile(project.getProjectDirectory());
}
if (pwd == null) {
pwd = FileUtil.toFile(rakeFile.getParent());
}
List<RakeTask> tasksToRun = Arrays.asList(tasks);
computeAndSetDisplayName(tasksToRun);
final List<ExecutionService> services = getExecutionServices(tasksToRun);
List<Future<Integer>> futures = new ArrayList<Future<Integer>>(services.size());
for (ExecutionService each : services) {
futures.add(each.run());
}
return futures;
}
private List<ExecutionService> getExecutionServices(List<? extends RakeTask> tasks) {
List<ExecutionService> result = new ArrayList<ExecutionService>(tasks.size());
for (RubyExecutionDescriptor descriptor : getDescriptors(tasks)) {
result.add(buildExecutionService(descriptor));
}
return result;
}
private ExecutionService buildExecutionService(RubyExecutionDescriptor descriptor) {
String charsetName = null;
if (project != null) {
PropertyEvaluator evaluator = project.getLookup().lookup(PropertyEvaluator.class);
if (evaluator != null) {
charsetName = evaluator.getProperty(SharedRubyProjectProperties.SOURCE_ENCODING);
}
}
// the original post build action to delegate to
final Runnable original = descriptor.getPostBuild();
// refresh file system after the task has finished as it may have
// created/deleted files
descriptor.postBuild(new Runnable() {
public void run() {
if (original != null) {
original.run();
}
FileUtil.refreshFor(FileUtil.toFile(project.getProjectDirectory()));
}
});
RubyProcessCreator processCreator = new RubyProcessCreator(descriptor, charsetName);
return ExecutionService.newService(processCreator, descriptor.toExecutionDescriptor(), displayName);
}
// package private for unit tests
List<RubyExecutionDescriptor> getDescriptors(List<? extends RakeTask> tasks) {
RubyPlatform platform = RubyPlatform.platformFor(project);
String rake = platform.getRake();
Collection<? extends RakeTaskCustomizer> customizers = Lookup.getDefault().lookupAll(RakeTaskCustomizer.class);
List<RubyExecutionDescriptor> result = new ArrayList<RubyExecutionDescriptor>(tasks.size());
RubyCoverageProvider coverageProvider = RubyCoverageProvider.get(project);
if (coverageProvider == null || !coverageProvider.isEnabled()) {
coverageProvider = null;
}
for (RakeTask task : tasks) {
RubyExecutionDescriptor desc = new RubyExecutionDescriptor(platform, displayName, pwd, rake);
doStandardConfiguration(desc);
String[] existingInitialArgs = desc.getInitialArgs() != null ? desc.getInitialArgs() : new String[0];
List<String> initialArgs = new ArrayList<String>(Arrays.asList(existingInitialArgs));
for (RakeTaskCustomizer customizer : customizers) {
customizer.customize(project, task, desc, debug);
}
initialArgs.addAll(task.getRakeParameters());
String resultingInitialArgs = "";
for (Iterator<String> it = initialArgs.iterator(); it.hasNext();) {
resultingInitialArgs += it.next();
if (it.hasNext()) {
resultingInitialArgs += " ";
}
}
desc.initialArgs(resultingInitialArgs);
List<String> additionalArgs = new ArrayList<String>();
String railsEnv = project.evaluator().getProperty(SharedRubyProjectProperties.RAILS_ENV);
if (railsEnv != null && !"".equals(railsEnv.trim())) {
additionalArgs.add("RAILS_ENV=" + railsEnv);//NOI18N
}
String[] existingAdditionalArgs = desc.getAdditionalArgs();
if (existingAdditionalArgs != null && existingAdditionalArgs.length > 0) {
additionalArgs.addAll(Arrays.asList(existingAdditionalArgs));
}
// additionalArgs.add(task.getTask());
addTaskParameters(task, parameters, additionalArgs);
//XXX: why exactly we have parameters both here and in RakeTask??
additionalArgs.addAll(task.getTaskParameters());
desc.additionalArgs(additionalArgs.toArray(new String[additionalArgs.size()]));
if (coverageProvider != null) {
desc = coverageProvider.wrapWithCoverage(desc, true, null);
}
result.add(desc);
}
return result;
}
private static void addTaskParameters(RakeTask task, List<String> parameters, List<String> additionalArgs) {
if (!task.acceptsExplicitParameters()) {
// old style (rake < 0.8.0) params
additionalArgs.add(task.getTask());
additionalArgs.addAll(parameters);
} else {
// the task explicitly declares the params
// it accepts, >= rake 0.8.0
StringBuilder paramsArg = new StringBuilder();
paramsArg.append(task.getTask());
if (!parameters.isEmpty()) {
Iterator<String> it = parameters.iterator();
paramsArg.append("[");
paramsArg.append(it.next());
while (it.hasNext()) {
paramsArg.append(",");
paramsArg.append(it.next());
}
paramsArg.append("]");
}
additionalArgs.add(paramsArg.toString());
}
}
private void doStandardConfiguration(RubyExecutionDescriptor desc) {
String charsetName = null;
String classPath = null;
String extraArgs = null;
String jrubyProps = null;
String options = null;
if (project != null) {
PropertyEvaluator evaluator = project.getLookup().lookup(PropertyEvaluator.class);
if (evaluator != null) {
charsetName = evaluator.getProperty(SharedRubyProjectProperties.SOURCE_ENCODING);
classPath = evaluator.getProperty(SharedRubyProjectProperties.JAVAC_CLASSPATH);
extraArgs = evaluator.getProperty(SharedRubyProjectProperties.RAKE_ARGS);
jrubyProps = evaluator.getProperty(RubyProjectProperties.JVM_ARGS);
options = evaluator.getProperty(RubyProjectProperties.RUBY_OPTIONS);
}
}
List<String> additionalArgs = new ArrayList<String>();
if (extraArgs != null) {
String[] args = Utilities.parseParameters(extraArgs);
if (args != null) {
for (String arg : args) {
additionalArgs.add(arg);
}
}
}
if (rakeFile != null) {
additionalArgs.add("-f"); // NOI18N
additionalArgs.add(FileUtil.toFile(rakeFile).getAbsolutePath());
}
if (!additionalArgs.isEmpty()) {
desc.additionalArgs(additionalArgs.toArray(new String[additionalArgs.size()]));
}
if (options != null) {
desc.initialArgs(options);
}
desc.allowInput();
desc.classPath(classPath); // Applies only to JRuby
desc.jvmArguments(jrubyProps);
desc.fileLocator(fileLocator);
desc.addStandardRecognizers();
if (RubyPlatform.platformFor(project).isJRuby()) {
desc.appendJdkToPath(true);
}
desc.addOutConvertor(new RakeErrorLineConvertor(desc, charsetName, displayName));
desc.addErrConvertor(new RakeErrorLineConvertor(desc, charsetName, displayName));
desc.debug(debug);
}
private void computeAndSetDisplayName(final List<? extends RakeTask> tasks) {
ProjectInformation info = ProjectUtils.getInformation(project);
String baseDisplayName = info == null ? NbBundle.getMessage(RakeRunnerAction.class, "RakeRunnerAction.Rake") : info.getDisplayName();
StringBuilder displayNameSB = new StringBuilder(baseDisplayName).append(" ("); // NOI18N
for (int i = 0; i < tasks.size(); i++) {
displayNameSB.append(tasks.get(i).getTask());
if (i != tasks.size() - 1) {
displayNameSB.append(", "); // NOI18N
}
}
displayNameSB.append(')');
setDisplayName(displayNameSB.toString()); // NOI18N
}
private static class RakeErrorLineConvertor implements LineConvertor {
private final RubyExecutionDescriptor template;
private final String charsetName;
private final String displayName;
public RakeErrorLineConvertor(RubyExecutionDescriptor desc, String charsetName, String displayName) {
this.template = desc;
this.charsetName = charsetName;
this.displayName = displayName;
}
public List<ConvertedLine> convert(String line) {
if (line.indexOf("(See full trace by running task with --trace)") != -1) { // NOI18N
return Collections.<ConvertedLine>singletonList(
ConvertedLine.forText(
NbBundle.getMessage(RakeRunner.class, "RakeSupport.RerunRakeWithTrace"),
new OutputListener() {
public void outputLineSelected(OutputEvent ev) {
}
public void outputLineAction(OutputEvent ev) {
RubyProcessCreator rpc = new RubyProcessCreator(buildDescriptor(), charsetName);
ExecutionService.newService(rpc, template.toExecutionDescriptor(), displayName).run();
}
private RubyExecutionDescriptor buildDescriptor() {
// copy the old args from template
String[] existing = template.getAdditionalArgs() != null ? template.getAdditionalArgs() : new String[0];
String[] args = new String[existing.length + 1];
for (int i = 0; i < existing.length; i++) {
args[i] = existing[i];
}
args[args.length - 1] = "--trace"; //NOI18N
return new RubyExecutionDescriptor(template).additionalArgs(args);
}
public void outputLineCleared(OutputEvent ev) {
}
}));
}
return null;
}
}
}