/*
* 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]"
*
* 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.glassfish.jruby;
import java.io.File;
import java.io.FileFilter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import javax.swing.JPanel;
import javax.swing.event.ChangeListener;
import org.netbeans.api.extexecution.print.ConvertedLine;
import org.netbeans.api.extexecution.print.LineConvertor;
import org.netbeans.api.extexecution.print.LineConvertors;
import org.netbeans.api.extexecution.print.LineConvertors.FileLocator;
import org.netbeans.api.ruby.platform.RubyPlatform;
import org.netbeans.modules.glassfish.jruby.ui.JRubyServerCustomizer;
import org.netbeans.modules.glassfish.spi.Recognizer;
import org.netbeans.modules.ruby.railsprojects.server.spi.RubyInstance;
import org.netbeans.modules.glassfish.spi.CustomizerCookie;
import org.netbeans.modules.glassfish.spi.GlassfishModule;
import org.netbeans.modules.glassfish.spi.OperationStateListener;
import org.netbeans.modules.glassfish.spi.RecognizerCookie;
import org.netbeans.modules.glassfish.spi.ServerCommand;
import org.netbeans.modules.glassfish.spi.ServerUtilities;
import org.netbeans.modules.glassfish.spi.Utils;
import org.netbeans.modules.ruby.platform.execution.DirectoryFileLocator;
import org.netbeans.modules.ruby.platform.execution.RubyLineConvertorFactory;
import org.openide.filesystems.FileUtil;
import org.openide.util.Lookup;
import org.openide.util.RequestProcessor;
import org.openide.windows.OutputListener;
/**
*
* @author Peter Williams
*/
public class JRubyServerModule implements RubyInstance, CustomizerCookie, RecognizerCookie {
public static final String USE_ROOT_CONTEXT_ATTR = "jruby.useRootContext"; // NOI18N
private Lookup lookup;
JRubyServerModule(Lookup instanceLookup) {
this.lookup = instanceLookup;
}
@Override
public String toString() {
return "GlassFish v3 Prelude / JRuby Support"; // NOI18N
}
// ------------------------------------------------------------------------
// RubyInstance implementation
// ------------------------------------------------------------------------
public String getServerUri() {
String serverUri = null;
GlassfishModule commonModule = lookup.lookup(GlassfishModule.class);
if(commonModule != null) {
serverUri = commonModule.getInstanceProperties().get(GlassfishModule.URL_ATTR);
} else {
Logger.getLogger("glassfish-jruby").log(Level.INFO,
"No V3 Common Server support found for V3/Ruby server instance");
}
return serverUri;
}
public String getDisplayName() {
String displayName = null;
GlassfishModule commonModule = lookup.lookup(GlassfishModule.class);
if(commonModule != null) {
displayName = commonModule.getInstanceProperties().get(GlassfishModule.DISPLAY_NAME_ATTR);
} else {
Logger.getLogger("glassfish-jruby").log(Level.INFO,
"No V3 Common Server support found for V3/Ruby server instance");
}
return displayName;
}
public ServerState getServerState() {
ServerState state = null;
GlassfishModule commonModule = lookup.lookup(GlassfishModule.class);
if(commonModule != null) {
state = translateServerState(commonModule.getServerState());
} else {
Logger.getLogger("glassfish-jruby").log(Level.INFO,
"No V3 Common Server support found for V3/Ruby server instance");
}
return state;
}
public Future<OperationState> startServer(final RubyPlatform platform) {
GlassfishModule commonModule = lookup.lookup(GlassfishModule.class);
if(commonModule != null) {
// !PW XXX check for pre-existing different platform
commonModule.setEnvironmentProperty(GlassfishModule.JRUBY_HOME,
platform.getHome().getAbsolutePath(), false);
return wrapTask(commonModule.startServer(new OperationStateListener() {
public void operationStateChanged(final GlassfishModule.OperationState newState, final String message) {
Logger.getLogger("glassfish-jruby").log(Level.FINEST,
"startServer V3/JRuby: " + newState + " - " + message);
}
}));
} else {
throw new IllegalStateException("No V3 Common Server support found for V3/Ruby server instance");
}
}
public Future<OperationState> runApplication(final RubyPlatform platform,
final String applicationName, final File applicationDir) {
GlassfishModule commonModule = lookup.lookup(GlassfishModule.class);
if(commonModule != null) {
// !PW XXX check for pre-existing different platform
String requestedPlatformDir = platform.getHome().getAbsolutePath();
String currentPlatformDir = commonModule.setEnvironmentProperty(
GlassfishModule.JRUBY_HOME, requestedPlatformDir, false);
if(!requestedPlatformDir.equals(currentPlatformDir)) {
// !PW XXX Server using different platform, fail until platform
// restart query implemented.
Logger.getLogger("glassfish-jruby").log(Level.WARNING,
"Running project with current V3 Rails platform " + currentPlatformDir +
" rather than requested platform " + requestedPlatformDir);
}
RubyPlatform.Info info = platform.getInfo();
commonModule.setEnvironmentProperty(GlassfishModule.GEM_HOME, info.getGemHome(), false);
commonModule.setEnvironmentProperty(GlassfishModule.GEM_PATH, info.getGemPath(), false);
GlassfishModule.ServerState state = commonModule.getServerState();
if(state == GlassfishModule.ServerState.STOPPED ||
state == GlassfishModule.ServerState.RUNNING) {
FutureTask<OperationState> task = new FutureTask<OperationState>(
new RunAppTask(commonModule, applicationName, applicationDir,
state == GlassfishModule.ServerState.STOPPED));
RequestProcessor.getDefault().post(task);
return task;
} else {
// !PW XXX server state indeterminate - starting or stopping,
// fail action until wait capability installed.
return failedOperation();
}
} else {
throw new IllegalStateException("No V3 Common Server support found for V3/Ruby server instance");
}
}
private static class RunAppTask implements
Callable<OperationState>,
OperationStateListener
{
private final GlassfishModule commonModule;
private final String applicationName;
private final File applicationDir;
private final boolean doStart;
private String contextRoot;
private String step;
public RunAppTask(final GlassfishModule module, final String appname, final File appdir, boolean startRequired) {
commonModule = module;
applicationName = appname.replaceAll("[ \t]", "_");
applicationDir = appdir;
contextRoot = calculateContextRoot(module, appname);
if("".equals(contextRoot)) {
contextRoot = "/";
}
doStart = startRequired;
step = "start";
}
public OperationState call() throws Exception {
GlassfishModule.OperationState result = GlassfishModule.OperationState.COMPLETED;
if(doStart) {
final Future<GlassfishModule.OperationState> startFuture =
commonModule.startServer(this);
result = startFuture.get();
}
if(result == GlassfishModule.OperationState.COMPLETED) {
if(isDeployed()) {
result = GlassfishModule.OperationState.COMPLETED;
} else {
step = "deploy";
Map<String,String> m = new HashMap<String,String>();
String jrubyHome = commonModule.getInstanceProperties().get(GlassfishModule.JRUBY_HOME);
if (null != jrubyHome && jrubyHome.length() > 0) {
m.put("jruby.home", Utils.escapePath(jrubyHome).replace(":", "\\:"));
} else {
throw new RuntimeException();
}
final Future<GlassfishModule.OperationState> deployFuture =
commonModule.deploy(this, applicationDir, applicationName, contextRoot, m);
result = deployFuture.get();
}
}
return translateOperationState(result);
}
public void operationStateChanged(final GlassfishModule.OperationState newState, final String message) {
Logger.getLogger("glassfish-jruby").log(Level.FINEST,
"runApplication/" + step + " V3/JRuby: " + newState + " - " + message);
}
private boolean isDeployed() {
step = "checkdeployed";
String propertyBase = "applications.application." + applicationName;
ServerCommand.GetPropertyCommand getCmd = new ServerCommand.GetPropertyCommand(propertyBase);
Future<GlassfishModule.OperationState> cmdOp = commonModule.execute(getCmd);
try {
GlassfishModule.OperationState result = cmdOp.get(15000, TimeUnit.MILLISECONDS);
if(result != GlassfishModule.OperationState.COMPLETED) {
return false;
}
Map<String, String> properties = getCmd.getData();
String name = properties.get(propertyBase + ".name");
if(name == null || !name.equals(applicationName)) {
return false;
}
String location = properties.get(propertyBase + ".location");
if(location == null || !location.startsWith("file:") ||
!match(applicationDir, location.substring(5))) {
return false;
}
String contextRootProperty = propertyBase + ".context-root";
String deployedContextRoot = properties.get(contextRootProperty);
if(deployedContextRoot == null) {
return false;
} else if(!deployedContextRoot.equals(contextRoot)) {
ServerCommand.SetPropertyCommand setCmd = commonModule.getCommandFactory().getSetPropertyCommand(
contextRootProperty, contextRoot);
result = commonModule.execute(setCmd).get(15000, TimeUnit.MILLISECONDS);
if(result != GlassfishModule.OperationState.COMPLETED) {
return false;
}
}
} catch(Exception ex) {
// Assume application is not deployed correctly. Not expected.
Logger.getLogger("glassfish-jruby").log(Level.FINE, ex.getLocalizedMessage(), ex);
return false;
}
return true;
}
private boolean match(File dir, String path) {
String dirpath = dir.getAbsolutePath().replaceAll("[ \t]", "%20");
if(!dirpath.endsWith("/")) {
dirpath = dirpath + "/";
}
if(!path.endsWith("/")) {
path = path + "/";
}
return dirpath.equals(path);
}
}
public Future<OperationState> stopServer() {
GlassfishModule commonModule = lookup.lookup(GlassfishModule.class);
if(commonModule != null) {
return wrapTask(commonModule.stopServer(new OperationStateListener() {
public void operationStateChanged(final GlassfishModule.OperationState newState, final String message) {
Logger.getLogger("glassfish-jruby").log(Level.FINEST,
"stopServer V3/JRuby: " + newState + " - " + message);
}
}));
} else {
throw new IllegalStateException("No V3 Common Server support found for V3/Ruby server instance");
}
}
public Future<OperationState> deploy(final String applicationName, final File applicationDir) {
GlassfishModule commonModule = lookup.lookup(GlassfishModule.class);
if(commonModule != null) {
FutureTask<OperationState> task = new FutureTask<OperationState>(
new RunAppTask(commonModule, applicationName, applicationDir, false));
RequestProcessor.getDefault().post(task);
return task;
} else {
throw new IllegalStateException("No V3 Common Server support found for V3/Ruby server instance");
}
}
public Future<OperationState> stop(final String applicationName) {
GlassfishModule commonModule = lookup.lookup(GlassfishModule.class);
if(commonModule != null) {
return wrapTask(
commonModule.undeploy(new OperationStateListener() {
public void operationStateChanged(final GlassfishModule.OperationState newState, final String message) {
Logger.getLogger("glassfish-jruby").log(Level.FINEST,
"undeploy V3/JRuby: " + newState + " - " + message);
}
}, applicationName.replaceAll("[ \t]", "_")));
} else {
throw new IllegalStateException("No V3 Common Server support found for V3/Ruby server instance");
}
}
public boolean isPlatformSupported(RubyPlatform platform) {
// Only JRuby platforms (but all of them, regardless of version, for now)
// are supported.
return platform.isJRuby();
}
public void addChangeListener(ChangeListener listener) {
GlassfishModule commonModule = lookup.lookup(GlassfishModule.class);
if(commonModule != null) {
commonModule.addChangeListener(listener);
} else {
Logger.getLogger("glassfish-jruby").log(Level.WARNING,
"No V3 Common Server support found for V3/Ruby server instance");
}
}
public void removeChangeListener(ChangeListener listener) {
GlassfishModule commonModule = lookup.lookup(GlassfishModule.class);
if(commonModule != null) {
commonModule.removeChangeListener(listener);
} else {
Logger.getLogger("glassfish-jruby").log(Level.WARNING,
"No V3 Common Server support found for V3/Ruby server instance");
}
}
public String getContextRoot(String applicationName) {
String result = applicationName;
GlassfishModule commonModule = lookup.lookup(GlassfishModule.class);
if(commonModule != null) {
result = calculateContextRoot(commonModule, applicationName);
}
return result;
}
public int getRailsPort() {
int httpPort = 8080; // defaults should probably be public in gfcommon spi
GlassfishModule commonModule = lookup.lookup(GlassfishModule.class);
if(commonModule != null) {
try {
String httpPortStr = commonModule.getInstanceProperties().get(GlassfishModule.HTTPPORT_ATTR);
httpPort = Integer.parseInt(httpPortStr);
} catch(NumberFormatException ex) {
Logger.getLogger("glassfish-jruby").log(Level.WARNING,
"Server's HTTP port value is not a valid integer.");
}
} else {
Logger.getLogger("glassfish-jruby").log(Level.WARNING,
"No V3 Common Server support found for V3/Ruby server instance");
}
return httpPort;
}
public String getServerCommand(final RubyPlatform platform, final String classpath,
final File applicationDir, final int httpPort, final boolean debug) {
/**
* -cp
* ${server}/modules/grizzly-jruby-1.7.8-SNAPSHOT.jar:
* ${server}/modules/grizzly-module-1.8.2.jar:
* ${jruby.home}/lib/jruby.jar
* -Djruby.home=${jruby.home}
* -client
* ${grizzly.jruby.vm.params}
* -Xdebug
* -Xrunjdwp:transport=dt_socket,address=${grizzly.jruby.vm.debugport},server=y,suspend=n
* -Dglassfish.rdebug=${rdebug.path}
* -Dglassfish.rdebug.port=${rdebug.port}
* -Dglassfish.rdebug.version=${rdebug.version}
* -Dglassfish.rdebug.iosynch=${rdebug.iosynch}
* com.sun.grizzly.standalone.JRuby
* -p <Rails HTTP Port> -n 1 <Rails Application Folder>
*/
StringBuilder builder = new StringBuilder(1000);
// JVM classpath
builder.append("-cp ");
List<String> serverJars = getServerJars();
for(String jarPath: serverJars) {
builder.append(jarPath);
builder.append(File.pathSeparatorChar);
}
String jrubyJar = platform.getHome().getAbsolutePath() +
File.separatorChar + "lib" + File.separatorChar + "jruby.jar";
builder.append(ServerUtilities.quote(jrubyJar));
if(classpath != null && classpath.length() > 0) {
String[] subpaths = classpath.split(File.pathSeparator);
for(String subpath: subpaths) {
builder.append(File.pathSeparatorChar);
builder.append(ServerUtilities.quote(subpath));
}
}
// JVM flags
builder.append(" -client");
// JVM properties
builder.append(" -Djruby.home=");
builder.append(ServerUtilities.quote(platform.getHome().getAbsolutePath()));
builder.append(" -Djruby.runtime.max=1");
String grizzlyVMParams = System.getProperty("grizzly.jruby.vm.params");
if(grizzlyVMParams != null) {
builder.append(' ');
builder.append(grizzlyVMParams);
}
// Enables debugging of the Grizzly JVM, if this system property is set.
Integer grizzlyVMDebugPort = Integer.getInteger("grizzly.jruby.vm.debugport");
if(grizzlyVMDebugPort != null) {
builder.append(" -Xdebug -Xrunjdwp:transport=dt_socket,address=" +
grizzlyVMDebugPort + ",server=y,suspend=y");
}
// Define properties to enable rdebug inside Grizzly/JRuby adapter if
// debugging is enabled.
if(debug) {
builder.append(" -Djruby.reflection=true -Djruby.compile.mode=OFF -Djruby.debug.fullTrace=true");
builder.append(
" -Dglassfish.rdebug=${rdebug.path}" +
" -Dglassfish.rdebug.port=${rdebug.port}" +
" -Dglassfish.rdebug.version=${rdebug.version}" +
" -Dglassfish.rdebug.iosynch=${rdebug.iosynch}");
if("true".equals(System.getProperty("glassfish.rdebug.verbose"))) {
builder.append(" -Dglassfish.rdebug.verbose=true");
}
}
// Arguments to Grizzly/JRuby standalone server
builder.append(" com.sun.grizzly.standalone.JRuby");
if(debug) {
builder.append(" --debug");
}
builder.append(" -p ");
builder.append(httpPort);
builder.append(" -n 1 ");
builder.append(ServerUtilities.quote(applicationDir.getAbsolutePath()));
return builder.toString();
}
private List<String> getServerJars() {
List<String> serverJars = new ArrayList<String>();
GlassfishModule commonModule = lookup.lookup(GlassfishModule.class);
if(commonModule != null) {
String glassfishRoot = commonModule.getInstanceProperties().get(GlassfishModule.GLASSFISH_FOLDER_ATTR);
File modulesDir = new File(glassfishRoot, "modules");
// !PW FIXME cache results so we don't keep looking this up.
//
// !PW FIXME could have more robust jar searching. We need to
// locate the following jars:
// grizzly-jruby-<version>.jar // grizzly jruby adapter
// grizzly-module-<version>.jar // grizzly http connector
// grizzly-optionals-<version>.jar // comet, etc.
//
File [] grizzlyJars = modulesDir.listFiles(new FileFilter() {
public boolean accept(File pathname) {
String name = pathname.getName();
return name.startsWith("grizzly") && name.endsWith(".jar");
}
});
if(grizzlyJars != null) {
for(File jar: grizzlyJars) {
Logger.getLogger("glassfish-jruby").log(Level.FINER,
"Found jar for grizzly path: " + jar.getAbsolutePath());
serverJars.add(ServerUtilities.quote(jar.getAbsolutePath()));
}
} else {
Logger.getLogger("glassfish-jruby").log(Level.WARNING,
"Problem accessing " + modulesDir.getAbsolutePath() +
" when searching for Grizzly Jars.");
}
} else {
Logger.getLogger("glassfish-jruby").log(Level.WARNING,
"No V3 Common Server support found for V3/Ruby server instance");
}
return serverJars;
}
private static String calculateContextRoot(GlassfishModule commonModule, String applicationName) {
boolean useRootContext = Boolean.valueOf(
commonModule.getInstanceProperties().get(USE_ROOT_CONTEXT_ATTR));
return useRootContext ? "" : "/" + applicationName.replaceAll("[ \t]", "_");
}
/**
* !PW XXX Is there a more efficient way to implement a failed future object?
*
* @return Future object that represents an immediate failed operation
*/
private static Future<OperationState> failedOperation() {
return new Future<OperationState>() {
public boolean cancel(boolean mayInterruptIfRunning) {
return false;
}
public boolean isCancelled() {
return false;
}
public boolean isDone() {
return true;
}
public OperationState get() throws InterruptedException, ExecutionException {
return OperationState.FAILED;
}
public OperationState get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
return OperationState.FAILED;
}
};
}
/**
* @param Future object from glassfish common operations
*
* @return Future object using ruby module state constants.
*/
private static Future<OperationState> wrapTask(
final Future<GlassfishModule.OperationState> task) {
return new Future<OperationState>() {
public boolean cancel(boolean mayInterruptIfRunning) {
return task.cancel(mayInterruptIfRunning);
}
public boolean isCancelled() {
return task.isCancelled();
}
public boolean isDone() {
return task.isDone();
}
public OperationState get() throws InterruptedException, ExecutionException {
GlassfishModule.OperationState state = task.get();
return translateOperationState(state);
}
public OperationState get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
GlassfishModule.OperationState state = task.get(timeout, unit);
return translateOperationState(state);
}
};
}
private static ServerState translateServerState(GlassfishModule.ServerState state) {
switch(state) {
case STARTING:
return ServerState.STARTING;
case RUNNING:
case RUNNING_JVM_DEBUG:
return ServerState.RUNNING;
case STOPPING:
return ServerState.STOPPING;
case STOPPED:
case STOPPED_JVM_BP:
return ServerState.STOPPED;
}
Logger.getLogger("glassfish-jruby").log(Level.INFO, "Invalid server state: " + state);
return ServerState.STOPPED;
}
private static OperationState translateOperationState(GlassfishModule.OperationState state) {
switch(state) {
case RUNNING:
return OperationState.RUNNING;
case COMPLETED:
return OperationState.COMPLETED;
case FAILED:
return OperationState.FAILED;
}
Logger.getLogger("glassfish-jruby").log(Level.INFO, "Invalid operation state: " + state);
return OperationState.FAILED;
}
// ------------------------------------------------------------------------
// CustomizerCookie implementation
// ------------------------------------------------------------------------
public Collection<JPanel> getCustomizerPages() {
Collection<JPanel> result = new LinkedList<JPanel>();
result.add(new JRubyServerCustomizer(lookup.lookup(GlassfishModule.class)));
return result;
}
// ------------------------------------------------------------------------
// RecognizerCookie support
// ------------------------------------------------------------------------
private static final String PREFIX = "(?:\\s+|(?:[^:\\s\\d]{1,20}:\\s*))?"; // NOI18N
private static final String DRIVE = "(?:\\S{1}:)"; // NOI18N
private static final String FILE_CHAR = "[^\\s\\[\\]\\:\\\"]"; // NOI18N
private static final String FILE = "(?:" + FILE_CHAR + "*)"; // NOI18N
private static final String LINE = "([1-9][0-9]*)"; // NOI18N
private static final String ROL = ".*\\s?"; // NOI18N
private static final String FILE_SEP = "[\\\\/]"; // NOI18N
private static final String LINE_SEP = "\\:"; // NOI18N
// DRIVE?(FILE_SEP FILE)+ LINE_SEP LINE ROL
private static final Pattern PATH_RECOGNIZER = Pattern.compile(
PREFIX + DRIVE + "?(" + FILE_SEP + FILE + ")+" + LINE_SEP + LINE + ROL); // NOI18N
public Collection<? extends Recognizer> getRecognizers() {
FileLocator locator = new DirectoryFileLocator(FileUtil.toFileObject(FileUtil.normalizeFile(new File("/")))); //NOI18N
LineConvertor convertor = LineConvertors.filePattern(locator, PATH_RECOGNIZER, RubyLineConvertorFactory.EXT_RE, 1, 2);
return Collections.singleton(wrapRubyRecognizer(convertor));
}
private Recognizer wrapRubyRecognizer(final LineConvertor convertor) {
return new Recognizer() {
public OutputListener processLine(String text) {
OutputListener result = null;
List<ConvertedLine> match = convertor.convert(text);
if (match != null && !match.isEmpty()) {
// relies on an implementation detail of FilePatternConvertor in that
// this assumes the returned listener to be an instance of FindFileListener
result = match.get(0).getListener();
}
return result;
}
};
}
}