/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 1997-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]"
*
* Contributor(s):
*
* The Original Software is NetBeans. The Initial Developer of the Original
* Software is Sun Microsystems, Inc. Portions Copyright 1997-2008 Sun
* Microsystems, Inc. All Rights Reserved.
*
* 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.
*/
package org.netbeans.modules.ruby.rubyproject.rake;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import org.netbeans.modules.ruby.platform.Util;
import org.netbeans.modules.ruby.rubyproject.*;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.Action;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.api.project.SourceGroup;
import org.netbeans.api.project.Sources;
import org.netbeans.api.ruby.platform.RubyInstallation;
import org.netbeans.api.ruby.platform.RubyPlatform;
import org.netbeans.modules.ruby.platform.execution.ExecutionUtils;
import org.netbeans.modules.ruby.platform.gems.GemManager;
import org.openide.NotifyDescriptor;
import org.openide.actions.CopyAction;
import org.openide.actions.CutAction;
import org.openide.actions.DeleteAction;
import org.openide.actions.FileSystemAction;
import org.openide.actions.OpenAction;
import org.openide.actions.PasteAction;
import org.openide.actions.PropertiesAction;
import org.openide.actions.RenameAction;
import org.openide.actions.ToolsAction;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileSystem;
import org.openide.filesystems.FileUtil;
import org.openide.loaders.DataObject;
import org.openide.loaders.DataObjectNotFoundException;
import org.openide.modules.InstalledFileLocator;
import org.openide.nodes.FilterNode;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.actions.SystemAction;
/**
* Supports Rake related operations.
*/
public final class RakeSupport {
private static final Logger LOGGER = Logger.getLogger(RakeSupport.class.getName());
/** File storing the 'rake -D' output. */
static final String RAKE_D_OUTPUT = "nbproject/private/rake-d.txt"; // NOI18N
/** Standard names used for Rakefile. */
static final String[] RAKEFILE_NAMES = new String[] {
"Rakefile", "Rakefile.rb", "rakefile", "rakefile.rb" // NOI18N
};
private final Project project;
public RakeSupport(Project project) {
this.project = project;
}
/**
* Tries to find Rakefile for a given project.
*
* @param project project to be searched
* @return found Rakefile or <code>null</code> if not found
*/
public static FileObject findRakeFile(final Project project) {
FileObject pwd = project.getProjectDirectory();
// See if we're in the right directory
FileObject result = findRakeFileIn(pwd);
if (result != null) {
return result;
}
// Try to adjust the directory to a folder which contains a rakefile
Sources src = ProjectUtils.getSources(project);
//TODO: needs to be generified
SourceGroup[] rubygroups = src.getSourceGroups(RubyProject.SOURCES_TYPE_RUBY);
if (rubygroups != null && rubygroups.length > 0) {
for (SourceGroup group : rubygroups) {
FileObject f = group.getRootFolder();
FileObject r = findRakeFileIn(f);
if (r != null) {
return r;
}
}
}
return null;
}
private static FileObject findRakeFileIn(FileObject folder) {
for (String s : RakeSupport.RAKEFILE_NAMES) {
FileObject f = folder.getFileObject(s);
if (f != null) {
// #179305 -- need to do case sensitive comparison
File file = FileUtil.toFile(f);
try {
String canonicalName = file.getCanonicalFile().getName();
if (file != null && s.equals(canonicalName)) {
// logging for #179305
LOGGER.log(Level.FINE, "Found rakefile: {0}, searching with: {1}. Full path: {2}",
new Object[]{canonicalName, s, file.getCanonicalPath()});
return f;
}
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}
}
return null;
}
/**
* Checks whether the give file is a main Rake file.
*/
public static boolean isMainRakeFile(FileObject fo) {
for (String s : RakeSupport.RAKEFILE_NAMES) {
if (s.equals(fo.getNameExt())) {
return true;
}
}
return false;
}
/**
* Checks whether the give file is a Rake file. Might be either {@link
* #isMainRakeFile main Rake file} or file with <tt>.rake</tt> extension.
*/
public static boolean isRakeFile(FileObject fo) {
if (!fo.getMIMEType().equals(RubyInstallation.RUBY_MIME_TYPE)) {
return false;
}
if (isMainRakeFile(fo)) {
return true;
}
if (fo.getExt().equalsIgnoreCase("rake")) { // NOI18N
return true;
}
return false;
}
/**
* Checks whether the exactly one {@link #isRakeFile Rake file} is selected
* in the given context.
*/
public static boolean isRakeFileSelected(final Lookup context) {
Collection<? extends DataObject> lookupAll = context.lookupAll(DataObject.class);
if (lookupAll.size() == 1) {
FileObject f = lookupAll.iterator().next().getPrimaryFile();
if (RakeSupport.isRakeFile(f)) {
return true;
}
}
return false;
}
public static void refreshTasks(final Project project) {
refreshTasks(project, true);
}
/**
* Runs 'rake -D' and writes the output into the {@link #RAKE_D_OUTPUT} if
* succeed.
*
* @param project project for which tasks are read
*/
public static void refreshTasks(final Project project, final boolean warn) {
final FileObject projectDir = project.getProjectDirectory();
try {
projectDir.getFileSystem().runAtomicAction(new FileSystem.AtomicAction() {
public void run() throws IOException {
FileObject rakeD = project.getProjectDirectory().getFileObject(RAKE_D_OUTPUT);
// clean old content
if (rakeD != null && rakeD.isValid() && rakeD.isData()) {
rakeD.delete();
}
}
});
} catch (IOException ioe) {
Exceptions.printStackTrace(ioe);
}
if (!RubyPlatform.hasValidRake(project, true)) {
return;
}
String rakeOutput = readRakeTasksOutput(project, warn);
if (rakeOutput != null) {
writeRakeTasks(project, rakeOutput);
}
}
/**
* Runs 'rake -D' and returns the output.
*
* @param project project for which tasks are read
* @return rake output; might be <tt>null</tt> when underlying rake command fails.
*/
private static String readRakeTasksOutput(final Project project, boolean warn) {
File pwd;
FileObject rakeFile = RakeSupport.findRakeFile(project);
if (rakeFile == null) {
pwd = FileUtil.toFile(project.getProjectDirectory());
} else {
pwd = FileUtil.toFile(rakeFile.getParent());
}
// Install the given gem
RubyPlatform platform = RubyPlatform.platformFor(project);
return dumpRakeTasksInfo(platform, pwd, warn);
// TODO: we are not able to parse complex Rakefile (e.g. rails'), using -P argument, yet
// sb.append(hiddenRakeRunner(cmd, rakeCmd, pwd, "-P"));
}
/**
* Returns namespace-task tree for the given project.
*/
public static Set<RakeTask> getRakeTaskTree(final Project project) {
return getRakeTaskTree(project, true);
}
public static Set<RakeTask> getRakeTaskTree(final Project project, final boolean withDescriptionOnly) {
RakeTaskReader rtreader = new RakeTaskReader(project);
return rtreader.getRakeTaskTree(withDescriptionOnly);
}
static Set<RakeTask> getRakeTasks(final Project project) {
return getRakeTasks(project, true);
}
/**
* Returns flat, namespace-ignoring, list of Rake tasks for the given
* project.
*/
static Set<RakeTask> getRakeTasks(final Project project, final boolean withDescriptionOnly) {
Set<RakeTask> taskTree = RakeSupport.getRakeTaskTree(project, withDescriptionOnly);
Set<RakeTask> tasks = new TreeSet<RakeTask>();
addTasks(tasks, taskTree);
return tasks;
}
private static void addTasks(final Set<RakeTask> flatAccumulator, final Set<RakeTask> taskTree) {
for (RakeTask task : taskTree) {
if (task.isNameSpace()) {
addTasks(flatAccumulator, task.getChildren());
} else {
flatAccumulator.add(task);
}
}
}
/**
* Returns {@link RakeTask} for the give <tt>task</tt>.
*
* @param project Ruby project
* @param task task to be find
* @return <tt>null</tt> if not found; {@link RakeTask} instance othewise
*/
public static RakeTask getRakeTask(final Project project, final String task) {
Set<RakeTask> tasks = getRakeTasks(project, false);
for (RakeTask rakeTask : tasks) {
if (rakeTask.getTask().equals(task)) {
return rakeTask;
}
}
return null;
}
private static String dumpRakeTasksInfo(RubyPlatform platform, File pwd, boolean warn) {
List<String> argList = new ArrayList<String>();
File cmd = platform.getInterpreterFile();
if (!cmd.getName().startsWith("jruby") || ExecutionUtils.launchJRubyScript()) { // NOI18N
argList.add(cmd.getPath());
}
argList.addAll(ExecutionUtils.getRubyArgs(platform));
File rakeTaskInfoScript = InstalledFileLocator.getDefault().locate(
"rake_tasks_info.rb", "org.netbeans.modules.ruby.project", false); // NOI18N
if (rakeTaskInfoScript == null) {
throw new IllegalStateException("Cannot locate rake_tasks_info.rb script"); // NOI18N
}
argList.add(rakeTaskInfoScript.getAbsolutePath());
String[] args = argList.toArray(new String[argList.size()]);
ProcessBuilder pb = new ProcessBuilder(args);
pb.directory(pwd);
pb.redirectErrorStream(false);
// PATH additions for JRuby etc.
ExecutionUtils.setupProcessEnvironment(pb.environment(), cmd.getParent(), false);
GemManager.adjustEnvironment(platform, pb.environment());
int exitCode = -1;
String stdout = null;
try {
ExecutionUtils.logProcess(pb);
Process process = pb.start();
stdout = readInputStream(process.getInputStream(), false);
String stderr = readInputStream(process.getErrorStream(), true);
exitCode = process.waitFor();
if (warn && exitCode != 0) {
LOGGER.severe("rake process failed (workdir: " + pwd + "):\nstdout:\n\n" + stdout + // NOI18N
"stderr:\n" + stderr); // NOI18N
Util.notifyLocalized(RakeSupport.class, "RakeSupport.rake.task.fetching.fails", // NOI18N
NotifyDescriptor.ERROR_MESSAGE, pwd, stderr);
return null;
}
if (stderr.length() > 0) {
LOGGER.warning("rake process warnings:\n\n" + stderr); // NOI18N
}
} catch (IOException ioe) {
Exceptions.printStackTrace(ioe);
} catch (InterruptedException ie) {
Exceptions.printStackTrace(ie);
}
return stdout;
}
private static String readInputStream(final InputStream is, final boolean readingErrors) {
StringBuilder sb = new StringBuilder();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader br = new BufferedReader(isr);
String line;
try {
while (true) {
line = br.readLine();
if (line == null) {
break;
}
if (!readingErrors && !line.contains("=")) { // not 'key=value' property
continue;
}
sb.append(line);
sb.append('\n');
}
} catch (IOException ioe) {
Exceptions.printStackTrace(ioe);
} finally {
try {
is.close();
} catch (IOException e) {
LOGGER.log(Level.WARNING, e.getLocalizedMessage(), e);
}
}
return sb.toString();
}
static void writeRakeTasks(Project project, final String rakeDOutput) {
final FileObject projectDir = project.getProjectDirectory();
try {
projectDir.getFileSystem().runAtomicAction(new FileSystem.AtomicAction() {
public void run() throws IOException {
FileObject rakeTasksFile = projectDir.getFileObject(RAKE_D_OUTPUT);
if (rakeTasksFile != null) {
rakeTasksFile.delete();
}
rakeTasksFile = FileUtil.createData(projectDir, RAKE_D_OUTPUT);
OutputStream os = rakeTasksFile.getOutputStream();
Writer writer = new BufferedWriter(new OutputStreamWriter(os));
try {
writer.write(rakeDOutput);
} finally {
writer.close();
}
}
});
} catch (IOException ioe) {
Exceptions.printStackTrace(ioe);
}
}
public static final class RakeNode extends FilterNode {
private Action[] actions;
public RakeNode(final FileObject rakeFO) throws DataObjectNotFoundException {
super(DataObject.find(rakeFO).getNodeDelegate());
}
public @Override Action[] getActions(boolean context) {
if (actions == null) {
actions = new Action[] {
SystemAction.get(OpenAction.class),
null,
SystemAction.get(CutAction.class),
SystemAction.get(CopyAction.class),
SystemAction.get(PasteAction.class),
null,
SystemAction.get(DeleteAction.class),
SystemAction.get(RenameAction.class),
null,
SystemAction.get(FileSystemAction.class),
null,
SystemAction.get(ToolsAction.class),
SystemAction.get(PropertiesAction.class),
};
}
return actions;
}
}
}