/*
* Copyright (c) 2009 The Jackson Laboratory
*
* This software was developed by Gary Churchill's Lab at The Jackson
* Laboratory (see http://research.jax.org/faculty/churchill).
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this software. If not, see <http://www.gnu.org/licenses/>.
*/
package org.jax.r.rintegration;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipInputStream;
import javax.swing.JDialog;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import org.jax.r.configuration.RApplicationConfigurationManager;
import org.jax.r.jaxbgenerated.RApplicationConfiguration;
import org.jax.r.jaxbgenerated.RApplicationStateType;
import org.jax.r.jaxbgenerated.RConfigurationType;
import org.jax.r.jaxbgenerated.RInstallationType;
import org.jax.r.jaxbgenerated.RLaunchConfigurationType;
import org.jax.r.jaxbgenerated.SystemPropertyType;
import org.jax.r.rintegration.gui.RHomeSelectorFrame;
import org.jax.util.ConfigurationUtilities;
import org.jax.util.TextWrapper;
import org.jax.util.TypeSafeSystemProperties;
import org.jax.util.concurrent.SafeAWTInvoker;
import org.jax.util.io.FileExtensionFilter;
import org.jax.util.io.FileUtilities;
import org.jax.virtualmachine.CommandLineVirtualMachineLauncher;
import org.jax.virtualmachine.VirtualMachineException;
import org.jax.virtualmachine.VirtualMachineLauncher;
import org.jax.virtualmachine.VirtualMachineSettings;
/**
* This class contains all of the major functionality needed to launch
* a main application allowing the user to select an appropriate R
* distribution.
* @author <A HREF="mailto:keith.sheppard@jax.org">Keith Sheppard</A>
*/
// TODO add support for command line arguments to launcher
public abstract class RLauncher
{
/**
* our logger
*/
private static final Logger LOG = Logger.getLogger(
RLauncher.class.getName());
/**
* Get the application entry point. This class must have a main(...)
* or the launch will fail.
* @return
* the application's main
*/
protected abstract Class<?> getApplicationMainClass();
/**
* This is the resource path for the classpath zip
* @return
* the resource path
*/
protected abstract String getClasspathZipFileResourcePath();
/**
* Getter for the application configuration manager
* @return
* the application configuration manager
*/
protected abstract RApplicationConfigurationManager getApplicationConfigurationManager();
/**
* Getter for the application name as it should be presented to the user
* @return
* the application name
*/
protected abstract String getReadableApplicationName();
/**
* System properties to add to the launch
* @return
* getter for the launch system properties
*/
private Properties getLaunchSystemProperties()
{
RApplicationConfiguration applicationConfiguration =
this.getApplicationConfigurationManager().getApplicationConfiguration();
Properties launchSystemProperties = new Properties();
for(SystemPropertyType currProperty: applicationConfiguration.getLaunchSystemProperty())
{
launchSystemProperties.put(
currProperty.getKey(),
currProperty.getValue());
}
return launchSystemProperties;
}
private Long getJavaMemoryLimitMegabytes()
{
RApplicationConfiguration applicationConfiguration =
this.getApplicationConfigurationManager().getApplicationConfiguration();
return applicationConfiguration.getJavaMemoryLimitMegabytes();
}
/**
* Get the classpath that should be used for this R launcher.
* @return
* the classpath
*/
private String getClasspath()
{
try
{
// expand the classpath zip
File baseDir = new ConfigurationUtilities().getBaseDirectory();
File classpathDir = new File(
baseDir,
TypeSafeSystemProperties.CLASS_PATH_PROP_NAME);
classpathDir.mkdirs();
ZipInputStream classpathZipStream = new ZipInputStream(
this.getClass().getResourceAsStream(
this.getClasspathZipFileResourcePath()));
FileUtilities.unzipToDirectory(
classpathZipStream,
classpathDir);
classpathZipStream.close();
if(LOG.isLoggable(Level.FINE))
{
LOG.fine(
"successfully expanded jars to: " +
classpathDir.getAbsolutePath());
}
FileExtensionFilter jarFilter = new FileExtensionFilter("jar");
File[] jarFiles = classpathDir.listFiles(jarFilter);
StringBuffer classpath = new StringBuffer();
for(int i = 0; i < jarFiles.length; i++)
{
classpath.append(jarFiles[i].getAbsolutePath());
if(i < jarFiles.length - 1)
{
// add a separator unless it's the last jar
classpath.append(File.pathSeparatorChar);
}
}
if(LOG.isLoggable(Level.FINE))
{
LOG.fine("built classpath: " + classpath.toString());
}
return classpath.toString();
}
catch(IOException ex)
{
LOG.log(Level.SEVERE,
"failed to build classpath",
ex);
return null;
}
}
/**
* Tell the user that we have detected a change in the R installation
* directory structure and ask them if they want to select a new
* installation.
* @return
* true if they do
*/
private boolean determineIfUserWantsToSelectRInstallation()
{
try
{
// TODO messages should be in a resource bundle
String message =
"A change in the R installation directory " +
"structure has been detected. Would you like to " +
"select a new R installation or keep using the " +
"currently selected installation?";
final JOptionPane optionPane = new JOptionPane(
TextWrapper.wrapText(
message,
TextWrapper.DEFAULT_DIALOG_COLUMN_COUNT));
String[] options = new String[] {
"Select an R Installation...",
"Keep Current Selection"};
optionPane.setOptions(options);
optionPane.setMessageType(JOptionPane.QUESTION_MESSAGE);
// jump through some swing thread-safety hoops
FutureTask<Object> futureSelectedValue =
new FutureTask<Object>(new Callable<Object>()
{
public Object call() throws Exception
{
JDialog dialog = optionPane.createDialog(
null,
"R Installation Change Detected");
dialog.pack();
dialog.setVisible(true);
return optionPane.getValue();
}
});
SwingUtilities.invokeLater(futureSelectedValue);
// figure out what the user just asked us to do
Object selectedValue = futureSelectedValue.get();
if(selectedValue == null)
{
if(LOG.isLoggable(Level.FINE))
{
LOG.fine(
"user closed \"R Installation Change Detected\" " +
"dialog without making a selection");
}
return false;
}
else if(selectedValue == JOptionPane.UNINITIALIZED_VALUE)
{
LOG.severe(
"unexpected condition. user has not yet made " +
"a choice");
return false;
}
else
{
if(selectedValue == options[0])
{
if(LOG.isLoggable(Level.FINE))
{
LOG.fine("user wants to select an installation");
}
return true;
}
else if(selectedValue == options[1])
{
if(LOG.isLoggable(Level.FINE))
{
LOG.fine("user doesn't want to select an installation");
}
return false;
}
else
{
LOG.severe(
"received unexpected selection value: " +
selectedValue);
return false;
}
}
}
catch(Exception ex)
{
LOG.log(Level.SEVERE,
"received exception while asking user about " +
"whether or not they wanted to select an R installation",
ex);
return false;
}
}
/**
* Launch with the given configuration
* @param launchConfiguration
* the configuration to launch with
*/
private void launchWithConfiguration(
RLaunchConfiguration launchConfiguration)
{
switch(launchConfiguration.getLaunchUsing())
{
case LAUNCH_USING_ENVIRONMENT:
{
// the user wants us to use the current environment so invoke
// main directly
LOG.severe(
"direct launching not yet supported");
}
break;
case LAUNCH_USING_SELECTED_INSTALLATION:
{
LOG.fine(
"the user wants to use selected installation, so " +
"we're going to use the application launcher");
RInstallation selectedInstallation =
launchConfiguration.getSelectedInstallation();
if(selectedInstallation == null)
{
final String errorMessage =
"Error detected during application launch " +
"(no selected R installation). Exiting application.";
LOG.severe(errorMessage);
RLauncher.showLaunchErrorNotification(errorMessage);
}
else
{
// take care of R_HOME
File rHomeFile =
selectedInstallation.getRHomeDirectory();
if(rHomeFile != null)
{
// take care of library path
File installationJavaLibFile =
selectedInstallation.getLibraryDirectory();
if(installationJavaLibFile != null)
{
// put it all together and launch the VM
VirtualMachineSettings vmSettings =
new VirtualMachineSettings();
vmSettings.getSystemProperties().putAll(
this.getLaunchSystemProperties());
vmSettings.setMainClassName(
this.getApplicationMainClass().getName());
vmSettings.setClasspath(
this.getClasspath());
Long javaMaxMemoryMegabytes = this.getJavaMemoryLimitMegabytes();
if(javaMaxMemoryMegabytes != null)
{
vmSettings.setUseDefaultMaxMemory(false);
vmSettings.setMaxMemoryMegabytes(
javaMaxMemoryMegabytes.longValue());
}
PlatformSpecificRFunctionsFactory platformSpecificFactory =
PlatformSpecificRFunctionsFactory.getInstance();
PlatformSpecificRFunctions platformSpecific =
platformSpecificFactory.getPlatformSpecificRFunctions();
platformSpecific.updateSettingsForRInstallation(
vmSettings,
selectedInstallation);
VirtualMachineLauncher launcher =
new CommandLineVirtualMachineLauncher();
try
{
launcher.launchVirtualMachine(vmSettings);
}
catch(VirtualMachineException ex)
{
final String errorMessage =
"Caught exception while attempting to " +
"launch application.";
LOG.log(Level.SEVERE,
errorMessage,
ex);
RLauncher.showLaunchErrorNotification(errorMessage);
}
}
else
{
final String errorMessage =
"Error detected during application launch " +
"(null java.library.path in configuration). " +
"Exiting application.";
LOG.severe(errorMessage);
RLauncher.showLaunchErrorNotification(errorMessage);
}
}
else
{
final String errorMessage =
"Error detected during application launch " +
"(null R_HOME in configuration). Exiting application.";
LOG.severe(errorMessage);
RLauncher.showLaunchErrorNotification(errorMessage);
}
}
}
break;
default:
{
final String errorMessage =
"Error detected during application launch. " +
"Exiting application.";
LOG.severe(errorMessage);
RLauncher.showLaunchErrorNotification(errorMessage);
}
break;
}
}
/**
* Asks the user to select an R launch configuration
* @param installDirStructure
* the R directory structure to use
* @return
* the user selection or null if they cancel
*/
private RLaunchConfiguration getLaunchConfigurationFromUser(
PlatformSpecificRFunctions installDirStructure)
{
final RHomeSelectorFrame rHomeSelectorFrame = new RHomeSelectorFrame(
installDirStructure);
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
rHomeSelectorFrame.pack();
rHomeSelectorFrame.setVisible(true);
}
});
return rHomeSelectorFrame.getSelectedLaunchConfiguration();
}
/**
* Convenience function for showing a launch error message dialog
* @param errorMessage
* the message
*/
protected static void showLaunchErrorNotification(
final String errorMessage)
{
RLauncher.showErrorNotification(
errorMessage,
"Launch Error Detected");
}
/**
* Convenience function for showing an error dialog
* @param errorMessage
* the message
* @param title
* the dialogs title
*/
protected static void showErrorNotification(
final String errorMessage,
final String title)
{
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
JOptionPane.showMessageDialog(
null,
TextWrapper.wrapText(
errorMessage,
TextWrapper.DEFAULT_DIALOG_COLUMN_COUNT),
title,
JOptionPane.ERROR_MESSAGE);
}
});
}
/**
* Launch the application, returning when the application completes.
*/
public void launchApplication()
{
// get a hold of the application configuration
RApplicationConfigurationManager configurationManager =
this.getApplicationConfigurationManager();
configurationManager.setSaveOnExit(false);
RApplicationConfiguration jaxbRApplicationConfiguration =
configurationManager.getApplicationConfiguration();
RConfigurationType jaxbRConfiguration =
jaxbRApplicationConfiguration.getRConfiguration();
RLaunchConfigurationType jaxbRLaunchConfiguration =
jaxbRConfiguration.getRLaunchConfiguration();
RApplicationStateType applicationState =
configurationManager.getApplicationState();
PlatformSpecificRFunctions rInstallDirStructure =
PlatformSpecificRFunctionsFactory.getInstance().getPlatformSpecificRFunctions();
if(rInstallDirStructure == null)
{
final String errorMessage =
"Your current operating system is not supported by " +
this.getReadableApplicationName() + ". " +
"Please see the " + this.getReadableApplicationName() +
" home page for more information";
LOG.severe(errorMessage);
RLauncher.showErrorNotification(
errorMessage,
"Unsupported Operating System");
}
else
{
RInstallationScanner rScanner = new RInstallationScanner();
RInstallation[] detectedInstallations =
rScanner.scanForRInstallations(rInstallDirStructure);
Arrays.sort(detectedInstallations);
// see if the detected installations have changed since our
// last startup
RInstallation[] prevDetectedInstallations =
RApplicationConfigurationManager.fromJaxbToNativeRInstallations(
applicationState.getDetectedRInstallation());
boolean detectedRInstallsHaveChanged = !Arrays.equals(
detectedInstallations,
prevDetectedInstallations);
if(detectedRInstallsHaveChanged)
{
// push the changes into the configuration file so it doesn't
// show up as a change again when the user restarts
List<RInstallationType> detectedInstallationsConfig =
applicationState.getDetectedRInstallation();
List<RInstallationType> newDetectedInstallationsConfig =
RApplicationConfigurationManager.fromNativeToJaxbRInstallations(
detectedInstallations);
detectedInstallationsConfig.clear();
detectedInstallationsConfig.addAll(
newDetectedInstallationsConfig);
configurationManager.saveApplicationConfiguration();
}
// scan for r directories
if(jaxbRLaunchConfiguration != null)
{
RInstallationType selectedInstallation =
jaxbRLaunchConfiguration.getSelectedRInstallation();
boolean selectedInstallationExists =
new File(selectedInstallation.getLibraryDirectory()).exists();
if(!selectedInstallationExists)
{
try
{
SafeAWTInvoker.safeInvokeAndWait(new Runnable()
{
public void run()
{
String message =
"The selected R installation has been " +
"removed. Please select a new installation " +
"or cancel launch";
JOptionPane.showMessageDialog(
null,
TextWrapper.wrapText(
message,
TextWrapper.DEFAULT_DIALOG_COLUMN_COUNT),
"Missing R Installation",
JOptionPane.WARNING_MESSAGE);
LOG.fine("user warning: " + message);
}
});
}
catch(Exception ex)
{
LOG.log(Level.SEVERE,
"received exception trying to display a " +
"message to the user",
ex);
}
RLaunchConfiguration launchConfiguration =
this.getLaunchConfigurationFromUser(rInstallDirStructure);
this.saveNewConfigurationAndLaunch(launchConfiguration);
}
else if(detectedRInstallsHaveChanged)
{
boolean userWantsToSelectRInstallation =
this.determineIfUserWantsToSelectRInstallation();
if(userWantsToSelectRInstallation)
{
if(LOG.isLoggable(Level.FINE))
{
LOG.fine("user wants to select R installation");
}
RLaunchConfiguration launchConfiguration =
this.getLaunchConfigurationFromUser(
rInstallDirStructure);
this.saveNewConfigurationAndLaunch(
launchConfiguration);
}
else
{
if(LOG.isLoggable(Level.FINE))
{
LOG.fine(
"user doesn't want to select an R " +
"installation even though the install " +
"dir changed");
}
this.launchWithJaxbConfiguration(
jaxbRLaunchConfiguration);
}
}
else
{
if(LOG.isLoggable(Level.FINE))
{
LOG.fine("R installations have not changed");
}
this.launchWithJaxbConfiguration(
jaxbRLaunchConfiguration);
}
}
else
{
if(LOG.isLoggable(Level.FINE))
{
LOG.fine(
"no current R launch configuration exists. " +
"prompting user");
}
RLaunchConfiguration launchConfiguration =
this.getLaunchConfigurationFromUser(rInstallDirStructure);
this.saveNewConfigurationAndLaunch(launchConfiguration);
}
}
}
/**
* save the given configuration and launch with it
* @param rLaunchConfiguration
* the launch configuration
*/
private void saveNewConfigurationAndLaunch(
RLaunchConfiguration rLaunchConfiguration)
{
if(rLaunchConfiguration != null)
{
// save away launch configuration to configuration
RApplicationConfigurationManager applicationConfigurationManager =
this.getApplicationConfigurationManager();
RApplicationConfiguration jaxbApplicationConfiguration =
this.getApplicationConfigurationManager().getApplicationConfiguration();
RLaunchConfigurationType jaxbRLaunchConfiguration =
RApplicationConfigurationManager.fromNativeToJaxbRLaunchConfiguration(
rLaunchConfiguration);
jaxbApplicationConfiguration.getRConfiguration().setRLaunchConfiguration(
jaxbRLaunchConfiguration);
applicationConfigurationManager.saveApplicationConfiguration();
applicationConfigurationManager.saveApplicationState();
// launch
this.launchWithConfiguration(rLaunchConfiguration);
}
else
{
if(LOG.isLoggable(Level.FINE))
{
LOG.fine(
"user canceled launch configuration. " +
"exiting...");
}
}
}
/**
* Launch using the given jaxb launch configuration.
* @param jaxbLaunchConfiguration
* the jaxb launch configuration
*/
private void launchWithJaxbConfiguration(RLaunchConfigurationType jaxbLaunchConfiguration)
{
RLaunchConfiguration launchConfiguration =
RApplicationConfigurationManager.fromJaxbToNativeRLaunchConfiguration(
jaxbLaunchConfiguration);
if(launchConfiguration != null)
{
this.launchWithConfiguration(launchConfiguration);
}
else
{
final String errorMessage =
"Fatal error detected in " + this.getReadableApplicationName() +
" launch configuration";
LOG.severe(errorMessage);
RLauncher.showErrorNotification(
errorMessage,
"Fatal Configuration Error");
}
}
}