/* * 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.railsprojects; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.logging.Logger; import javax.swing.text.JTextComponent; import org.netbeans.api.extexecution.ExecutionService; import org.netbeans.api.extexecution.print.LineConvertor; import org.netbeans.api.extexecution.print.LineConvertors; import org.netbeans.api.ruby.platform.RubyInstallation; import org.netbeans.api.ruby.platform.RubyPlatform; import org.netbeans.modules.csl.api.DeclarationFinder.DeclarationLocation; import org.netbeans.modules.csl.spi.GsfUtilities; import org.netbeans.modules.ruby.AstUtilities; import org.netbeans.modules.ruby.RubyUtils; import org.netbeans.modules.ruby.codecoverage.RubyCoverageProvider; import org.netbeans.modules.ruby.platform.execution.RubyExecutionDescriptor; import org.netbeans.modules.ruby.platform.execution.RubyLineConvertorFactory; import org.netbeans.modules.ruby.platform.execution.RubyProcessCreator; import org.netbeans.modules.ruby.rubyproject.Migrations; import org.netbeans.modules.ruby.railsprojects.server.RailsServerManager; import org.netbeans.modules.ruby.railsprojects.ui.customizer.RailsProjectProperties; import org.netbeans.modules.ruby.rhtml.lexer.api.RhtmlTokenId; import org.netbeans.modules.ruby.rubyproject.AutoTestSupport; import org.netbeans.modules.ruby.rubyproject.GotoTest; import org.netbeans.modules.ruby.rubyproject.RSpecSupport; import org.netbeans.modules.ruby.rubyproject.RubyBaseActionProvider; import org.netbeans.modules.ruby.rubyproject.RubyFileLocator; import org.netbeans.modules.ruby.rubyproject.RubyProjectUtil; import org.netbeans.modules.ruby.rubyproject.SharedRubyProjectProperties; import org.netbeans.modules.ruby.rubyproject.TestNotifierLineConvertor; import org.netbeans.modules.ruby.rubyproject.UpdateHelper; import org.netbeans.modules.ruby.rubyproject.Util; import org.netbeans.modules.ruby.rubyproject.rake.RakeRunner; import org.netbeans.modules.ruby.rubyproject.rake.RakeSupport; import org.netbeans.modules.ruby.rubyproject.spi.TestRunner; import org.netbeans.modules.ruby.rubyproject.spi.TestRunner.TestType; import org.netbeans.modules.web.client.tools.api.WebClientToolsProjectUtils; import org.netbeans.modules.web.client.tools.api.WebClientToolsSessionStarterService; import org.netbeans.spi.project.ui.support.DefaultProjectOperations; 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.Utilities; import org.openide.windows.TopComponent; import org.openide.windows.WindowManager; /** * Action provider of the Ruby project. */ public final class RailsActionProvider extends RubyBaseActionProvider { private static final Logger LOGGER = Logger.getLogger(RailsActionProvider.class.getName()); /** * Standard command for running the Rails console for a project */ public static final String COMMAND_RAILS_CONSOLE = "rails-console"; // NOI18N // Commands available from Ruby project private static final String[] supportedActions = { COMMAND_AUTOTEST, COMMAND_AUTOSPEC, COMMAND_RDOC, COMMAND_RAILS_CONSOLE, COMMAND_RUN, COMMAND_RUN_SINGLE, COMMAND_DEBUG, COMMAND_DEBUG_SINGLE, COMMAND_TEST, COMMAND_RSPEC, COMMAND_TEST_SINGLE, COMMAND_DEBUG_TEST_SINGLE, COMMAND_DELETE, COMMAND_COPY, COMMAND_MOVE, COMMAND_RENAME, }; private final static String[] MIME_TYPES = new String[] { RubyInstallation.RUBY_MIME_TYPE, RhtmlTokenId.MIME_TYPE }; final RailsProject project; public RailsActionProvider(RailsProject project, UpdateHelper updateHelper) { super(project, updateHelper); this.project = project; } @Override protected FileObject[] getSourceRoots() { return project.getSourceRoots().getRoots(); } @Override protected FileObject[] getTestSourceRoots() { return project.getTestSourceRoots().getRoots(); } @Override protected String[] getMimeTypes() { return MIME_TYPES; } public String[] getSupportedActions() { return supportedActions; } /** Return true iff the given file is a migration file */ private boolean isMigrationFile(FileObject file) { if (file.getParent() == null || !file.getParent().getName().equals("migrate")) { // NOI18N return false; } if (file.getParent().getParent() == null || !file.getParent().getParent().getName().equals("db")) { // NOI18N return false; } if (!file.getMIMEType().equals(RubyInstallation.RUBY_MIME_TYPE)) { return false; } return Migrations.getMigrationVersion(file.getName()) != null; } public void invokeAction( final String command, final Lookup context ) throws IllegalArgumentException { // TODO Check for valid installation of Ruby and Rake RubyPlatform platform = RubyPlatform.platformFor(project); assert platform != null : "Action '" + command + "' should be disabled when platform is invalid"; boolean debugCommand = COMMAND_DEBUG.equals(command); boolean debugSingleCommand = COMMAND_DEBUG_SINGLE.equals(command); if (COMMAND_RUN.equals(command) || debugCommand) { // Save all files first LifecycleManager.getDefault().saveAll(); runServer("", debugCommand); return; } else if (COMMAND_TEST.equals(command)) { TestRunner testRunner = Util.getTestRunner(TestRunner.TestType.TEST_UNIT); boolean testTaskExist = RakeSupport.getRakeTask(project, TEST_TASK_NAME) != null; if (testTaskExist) { File pwd = FileUtil.toFile(project.getProjectDirectory()); RakeRunner runner = new RakeRunner(project); runner.setPWD(pwd); runner.setFileLocator(new RailsFileLocator(context, project)); runner.showWarnings(true); runner.setDebug(COMMAND_DEBUG_SINGLE.equals(command)); runner.run(TEST_TASK_NAME); // NOI18N } else if (testRunner != null) { testRunner.getInstance().runAllTests(project, false); } return; } else if (COMMAND_TEST_SINGLE.equals(command) || COMMAND_DEBUG_TEST_SINGLE.equals(command)) { if (!platform.isValid(true)) { return; } // Run test normally - don't pop up browser FileObject file = getCurrentFile(context); if (file == null) { return; } saveFile(file); // If we try to "test" a file that has a corresponding test file, // run/debug the test file instead DeclarationLocation location = new GotoTest().findTest(file, -1); if (location != DeclarationLocation.NONE) { file = location.getFileObject(); // Save the test file too saveFile(file); } else if (RubyUtils.isRhtmlFile(file)) { // Can't run RHTML files if there's no corresponding test return; } boolean isDebug = COMMAND_DEBUG_TEST_SINGLE.equals(command); RSpecSupport rspec = new RSpecSupport(project); if (rspec.isRSpecInstalled() && RSpecSupport.isSpecFile(file)) { TestRunner rspecRunner = Util.getTestRunner(TestRunner.TestType.RSPEC); if (rspecRunner != null) { rspecRunner.runTest(file, isDebug); } else { rspec.runRSpec(null, file, file.getName(), new RailsFileLocator(context, project), true, isDebug); } return; } TestRunner testRunner = Util.getTestRunner(TestRunner.TestType.TEST_UNIT); if (testRunner != null) { testRunner.getInstance().runTest(file, isDebug); } else { runRubyScript(file, FileUtil.toFile(file).getAbsolutePath(), file.getNameExt(), context, isDebug, new TestNotifierLineConvertor(true, true)); } return; } else if (COMMAND_RUN_SINGLE.equals(command) || debugSingleCommand) { if (!platform.isValid(true)) { return; } FileObject file = getCurrentFile(context); if (file == null) { return; } if (RakeSupport.isRakeFile(file)) { // Save all files first - this rake file could be accessing other files LifecycleManager.getDefault().saveAll(); RakeRunner runner = new RakeRunner(project); runner.setRakeFile(file); runner.setFileLocator(new RailsFileLocator(context, project)); runner.showWarnings(true); runner.setDebug(COMMAND_DEBUG_SINGLE.equals(command)); runner.run(); return; } RSpecSupport rspec = new RSpecSupport(project); if (rspec.isRSpecInstalled() && RSpecSupport.isSpecFile(file)) { TestRunner rspecRunner = Util.getTestRunner(TestRunner.TestType.RSPEC); boolean debug = COMMAND_DEBUG_SINGLE.equals(command); if (rspecRunner != null) { saveFile(file); rspecRunner.runTest(file, debug); } else { // Save all files first - this rake file could be accessing other files LifecycleManager.getDefault().saveAll(); rspec.runRSpec(null, file, file.getName(), new RailsFileLocator(context, project), true, debugSingleCommand); } return; } saveFile(file); if (isMigrationFile(file)) { String name = file.getName(); Long version = Migrations.getMigrationVersion(name); RakeRunner runner = new RakeRunner(project); runner.setPWD(FileUtil.toFile(project.getProjectDirectory())); runner.setFileLocator(new RailsFileLocator(context, project)); runner.showWarnings(true); runner.setParameters("VERSION=" + version); // NOI18N runner.run("db:migrate"); // NOI18N return; } // Try to bring up the relevant URL String path = ""; String fileName = file.getName(); final String CONTROLLER_SUFFIX = "_controller"; // NOI18N final String HELPER_SUFFIX = "_helper"; // NOI18N if (file.getExt().equals("rhtml") || file.getExt().equals("erb")) { // NOI18N if (fileName.endsWith(".html")) { // .html.erb // NOI18N fileName = fileName.substring(0, fileName.length()-".html".length()); // NOI18N } if (!fileName.startsWith("_")) { // NOI18N // For partials like "_foo", just use the surrounding view path = fileName; } FileObject projDir = project.getProjectDirectory(); FileObject curr = file.getParent(); while (curr != null && curr != projDir) { if (curr.getName().equals("views") && // NOI18N (curr.getParent() == null || curr.getParent().getName().equals("app"))) { // NOI18N break; } path = curr.getNameExt() + "/" + path; // NOI18N curr = curr.getParent(); } } else if (fileName.endsWith(CONTROLLER_SUFFIX)) { path = fileName.substring(0, fileName.length()-CONTROLLER_SUFFIX.length()); FileObject projDir = project.getProjectDirectory(); FileObject app = file.getParent(); while (app != null && app != projDir) { if (app.getName().equals("controllers") && // NOI18N (app.getParent() == null || app.getParent().getName().equals("app"))) { // NOI18N app = app.getParent(); break; } path = app.getNameExt() + "/" + path; // NOI18N app = app.getParent(); } // Try to find out which view we're in JTextComponent pane = GsfUtilities.getOpenPane(); if (app != null && pane != null && pane.getCaret() != null) { FileObject fo = GsfUtilities.findFileObject(pane); if (fo != null) { int offset = pane.getCaret().getDot(); if (offset >= 0) { String methodName = AstUtilities.getMethodName(file, offset); if (methodName != null) { // Make sure that this corresponds to a valid view - it could // be an unrelated method in the controller // - but what if it's a method which does rendering?? // String[] exts = { ".rhtml", ".html.erb", ".erb" }; // NOI18N // for (String ext : exts) { // FileObject viewFile = app.getFileObject("views/" + path + // NOI18N // "/" + methodName + ext); // NOI18N // if (viewFile != null) { // path = path + "/" + methodName; // NOI18N // break; // } // } path = path + "/" + methodName; // NOI18N } } } } } else if (fileName.endsWith(HELPER_SUFFIX)) { path = fileName.substring(0, fileName.length()-HELPER_SUFFIX.length()); FileObject projDir = project.getProjectDirectory(); FileObject curr = file.getParent(); while (curr != null && curr != projDir) { if (curr.getName().equals("helpers") && // NOI18N (curr.getParent() == null || curr.getParent().getName().equals("app"))) { // NOI18N break; } path = curr.getNameExt() + "/" + path; // NOI18N curr = curr.getParent(); } //} else if (parentName.equals("models")) { // NOI18N // // XXX What do we do here? } else if (fileName.endsWith("_test")) { // NOI18N // Run test normally - don't pop up browser TestRunner testRunner = Util.getTestRunner(TestRunner.TestType.TEST_UNIT); if (testRunner != null) { testRunner.getInstance().runTest(file, COMMAND_DEBUG_SINGLE.equals(command)); } else { runRubyScript(file, FileUtil.toFile(file).getAbsolutePath(), file.getNameExt(), context, debugSingleCommand, new TestNotifierLineConvertor(true, true)); } return; } if (path.length() == 0) { // No corresponding URL - some other file we should just try to execute runRubyScript(file, FileUtil.toFile(file).getAbsolutePath(), file.getNameExt(), context, debugSingleCommand); return; } runServer(path, debugCommand || debugSingleCommand); return; } if (COMMAND_RSPEC.equals(command)) { boolean rspecTaskExists = RakeSupport.getRakeTask(project, RSPEC_TASK_NAME) != null; TestRunner testRunner = Util.getTestRunner(TestRunner.TestType.RSPEC); if (rspecTaskExists) { File pwd = FileUtil.toFile(project.getProjectDirectory()); RakeRunner runner = new RakeRunner(project); runner.setPWD(pwd); runner.setFileLocator(new RubyFileLocator(context, project)); runner.showWarnings(true); runner.run(RSPEC_TASK_NAME); // NOI18N } else if (testRunner != null) { testRunner.getInstance().runAllTests(project, false); } return; } if (COMMAND_RSPEC_ALL.equals(command)) { TestRunner testRunner = Util.getTestRunner(TestRunner.TestType.RSPEC); if (testRunner != null) { testRunner.getInstance().runAllTests(project, false); } return; } if (COMMAND_AUTOTEST.equals(command)) { if (AutoTestSupport.isInstalled(project, TestType.AUTOTEST)) { AutoTestSupport support = new AutoTestSupport(context, project, getSourceEncoding()); support.setClassPath(project.evaluator().getProperty(RailsProjectProperties.JAVAC_CLASSPATH)); support.start(TestType.AUTOTEST); } return; } if (COMMAND_AUTOSPEC.equals(command)) { if (AutoTestSupport.isInstalled(project, TestType.AUTOSPEC)) { AutoTestSupport support = new AutoTestSupport(context, project, getSourceEncoding()); support.setClassPath(project.evaluator().getProperty(RailsProjectProperties.JAVAC_CLASSPATH)); support.start(TestType.AUTOSPEC); } return; } if (COMMAND_RAILS_CONSOLE.equals(command)) { //File pwd = FileUtil.toFile(project.getProjectDirectory()); //String cmd = "Dir.chdir('" + pwd.getAbsolutePath() + "');load 'script/console'"; // NOI18N //IrbTopComponent component = IrbTopComponent.getProjectConsole(project, cmd); // //component.open(); openRailsConsole(context); return; } if (COMMAND_DELETE.equals(command)) { DefaultProjectOperations.performDefaultDeleteOperation(project); return ; } if (COMMAND_COPY.equals(command)) { DefaultProjectOperations.performDefaultCopyOperation(project); return ; } if (COMMAND_MOVE.equals(command)) { DefaultProjectOperations.performDefaultMoveOperation(project); return ; } if (COMMAND_RENAME.equals(command)) { DefaultProjectOperations.performDefaultRenameOperation(project, null); return ; } } private void openRailsConsole(Lookup context) { String displayName = NbBundle.getMessage(RailsActionProvider.class, "RailsConsole"); File pwd = FileUtil.toFile(project.getProjectDirectory()); String script = null; List<String> additionalArgs = new ArrayList<String>(); boolean rails3 = RailsProjectUtil.getRailsVersion(project).isRails3OrHigher(); if (rails3) { script = "script" + File.separator + "rails"; // NOI18N additionalArgs.add("console"); } else { script = "script" + File.separator + "console"; // NOI18N } String classPath = project.evaluator().getProperty(RailsProjectProperties.JAVAC_CLASSPATH); // --irb not supported in rails3 if (!rails3) { if (Utilities.isWindows() && !getPlatform().isJRuby()) { // see #133066 additionalArgs.add("--irb=irb.bat --noreadline"); //NOI18N } else { additionalArgs.add("--irb=irb --noreadline"); //NOI18N } } String railsEnv = project.evaluator().getProperty(RailsProjectProperties.RAILS_ENV); if (railsEnv != null && !"".equals(railsEnv.trim())) { additionalArgs.add(railsEnv); } RubyExecutionDescriptor descriptor = new RubyExecutionDescriptor(getPlatform(), displayName, pwd, script). showSuspended(false). showProgress(false). classPath(classPath). allowInput(). // see #130264 additionalArgs(additionalArgs.toArray(new String[additionalArgs.size()])). //NOI18N fileLocator(new RailsFileLocator(context, project)); descriptor.addStandardRecognizers(); RubyProcessCreator rpc = new RubyProcessCreator(descriptor, getSourceEncoding()); ExecutionService.newService(rpc, descriptor.toExecutionDescriptor(), displayName).run(); // request focus for the output window - see #133519 getOutputWindow().requestActive(); } private static TopComponent getOutputWindow() { final String outputWindowId = "output"; //NOI18N TopComponent outputWindow = WindowManager.getDefault().findTopComponent(outputWindowId); assert outputWindow != null : "Could not find the output window using id " + outputWindowId; //NOI18N return outputWindow; } public RubyExecutionDescriptor getScriptDescriptor(File pwd, FileObject fileObject, String target, String displayName, final Lookup context, final boolean debug, LineConvertor... extraConvertors) { String rubyOptions = SharedRubyProjectProperties.getRubyOptions(project); String includePath = RubyProjectUtil.getLoadPath(project); if (rubyOptions != null) { rubyOptions = includePath + " " + rubyOptions; // NOI18N } else { rubyOptions = includePath; } FileObject[] srcPath = project.getSourceRoots().getRoots(); FileObject[] testPath = project.getTestSourceRoots().getRoots(); // Locate the target and specify it by full path. // This is necessary because JRuby and Ruby don't locate the script from the load // path it seems. if (!new File(target).exists() && srcPath != null && srcPath.length > 0) { boolean found = false; // Prefer the first match for (FileObject root : srcPath) { FileObject fo = root.getFileObject(target); if (fo != null) { target = FileUtil.toFile(fo).getAbsolutePath(); found = true; break; } } if (!found && testPath != null) { for (FileObject root : testPath) { FileObject fo = root.getFileObject(target); if (fo != null) { target = FileUtil.toFile(fo).getAbsolutePath(); break; } } } } // For Rails, the execution directory should be the RAILS_ROOT directory if (pwd == null) { pwd = FileUtil.toFile(project.getProjectDirectory()); } String classPath = project.evaluator().getProperty(RailsProjectProperties.JAVAC_CLASSPATH); String jvmArgs = project.evaluator().getProperty(RailsProjectProperties.JVM_ARGS); RubyExecutionDescriptor desc = new RubyExecutionDescriptor(getPlatform(), displayName, pwd, target); desc.debug(debug); desc.showSuspended(true); desc.allowInput(); desc.initialArgs(rubyOptions); desc.jvmArguments(jvmArgs); desc.classPath(classPath); desc.additionalArgs(getApplicationArguments()); desc.fileLocator(new RailsFileLocator(context, project)); desc.addStandardRecognizers(); desc.addOutConvertor(LineConvertors.filePattern(desc.getFileLocator(), RubyLineConvertorFactory.RUBY_TEST_OUTPUT, RubyLineConvertorFactory.EXT_RE ,1,2)); desc.addErrConvertor(LineConvertors.filePattern(desc.getFileLocator(), RubyLineConvertorFactory.RUBY_TEST_OUTPUT, RubyLineConvertorFactory.EXT_RE ,1,2)); if (extraConvertors != null) { for (LineConvertor extra : extraConvertors) { desc.addOutConvertor(extra); } } RubyCoverageProvider coverageProvider = RubyCoverageProvider.get(project); if (coverageProvider != null && coverageProvider.isEnabled()) { desc = coverageProvider.wrapWithCoverage(desc, false, null); } return desc; } public boolean isActionEnabled( String command, Lookup context ) { if (getPlatform() == null) { return false; } // We don't require files to be in the source roots to be executable/debuggable; // for example, in Rails you may want to switch to the Files view and execute // some of the files in scripts/, even though these are not considered sources // (and don't have a source root) return true; } // From the ant module - ActionUtils. // However, I've modified it to do its search based on mime type rather than // file suffixes (since some Ruby files do not use a .rb extension and are // discovered based on the initial shebang line) private void runServer(final String path, final boolean debug) { if (!debug) { runServer(path, false, false); } else { boolean serverDebug; boolean clientDebug; if (!WebClientToolsSessionStarterService.isAvailable()) { // Ignore the debugging options if no Javascript debugger is present clientDebug = false; serverDebug = true; } else { // show Debug Project dialog boolean keepDebugging = WebClientToolsProjectUtils.showDebugDialog(project); if (!keepDebugging) { return; } serverDebug = WebClientToolsProjectUtils.getServerDebugProperty(project); clientDebug = WebClientToolsProjectUtils.getClientDebugProperty(project); } assert serverDebug || clientDebug; runServer(path, serverDebug, clientDebug); } } private void runServer(String path, final boolean serverDebug, final boolean clientDebug) { TopComponent outputWindow = getOutputWindow(); if (!outputWindow.isOpened()) { outputWindow.open(); } outputWindow.requestActive(); outputWindow.toFront(); RailsServerManager server = project.getLookup().lookup(RailsServerManager.class); if (server != null) { server.setDebug(serverDebug); server.setClientDebug(clientDebug); // use the url from project config if no path is specified if (path == null || "".equals(path)) { //NOI18N String url = project.evaluator().getProperty(RailsProjectProperties.RAILS_URL); if (url != null) { path = url; } } server.showUrl(path); } } }