/* * Copyright 2009-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.codehaus.groovy.eclipse.launchers; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.SortedSet; import java.util.TreeSet; import org.codehaus.groovy.eclipse.GroovyPlugin; import org.codehaus.groovy.eclipse.core.GroovyCore; import org.codehaus.groovy.eclipse.core.model.GroovyProjectFacade; import org.codehaus.groovy.eclipse.core.preferences.PreferenceConstants; import org.codehaus.groovy.eclipse.core.util.ListUtil; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.core.runtime.IPath; import org.eclipse.debug.core.DebugPlugin; import org.eclipse.debug.core.ILaunchConfiguration; import org.eclipse.debug.core.ILaunchConfigurationType; import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; import org.eclipse.debug.core.ILaunchManager; import org.eclipse.debug.ui.DebugUITools; import org.eclipse.debug.ui.IDebugModelPresentation; import org.eclipse.debug.ui.ILaunchShortcut; import org.eclipse.jdt.core.IClasspathEntry; import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; import org.eclipse.jdt.launching.JavaRuntime; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.PlatformUI; /** * @author Andrew Eisenberg * @created Oct 7, 2009 */ public abstract class AbstractGroovyLaunchShortcut implements ILaunchShortcut { /** * Used for dialog presentation if the user needs to choose from * matching Launch configurations */ public static final String SELECT_CONFIG_DIALOG_TITLE = "Select Groovy {0} to launch"; /** * Used for dialog presentation if the user needs to choose from * matching Launch configurations */ public static final String SELECT_CONFIG_DIALOG_TEXT = "Select the Groovy {0} to launch"; /** * This is the string that will show if the groovy file the user is trying to run * doesn't meet the criteria to be run. */ public static final String GROOVY_FILE_NOT_RUNNABLE_MESSAGE = "Groovy {0} not found in current selection"; public static final String GROOVY_TYPE_TO_RUN = "org.codehaus.groovy.eclipse.launch.runType"; private final String title; private final String text; private final String msg; public AbstractGroovyLaunchShortcut() { title = SELECT_CONFIG_DIALOG_TITLE.replace("{0}", applicationOrConsole()); text = SELECT_CONFIG_DIALOG_TEXT.replace("{0}", applicationOrConsole()); msg = GROOVY_FILE_NOT_RUNNABLE_MESSAGE.replace("{0}", applicationOrConsole()); } /** * Launches from the package explorer. * * @see ILaunchShortcut#launch */ public void launch(ISelection selection, String mode) { ICompilationUnit unit = extractCompilationUnit(selection); IJavaProject javaProject; if (unit != null) { javaProject = unit.getJavaProject(); } else { javaProject = extractJavaProject(selection); } if (javaProject==null && unit==null) { MessageDialog.openError(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(), "Can't run script", "No script or project selected!"); return; } if (unit != null || canLaunchWithNoType()) { launchGroovy(unit, javaProject, mode); } else { MessageDialog.openError(PlatformUI.getWorkbench(). getActiveWorkbenchWindow().getShell(), "Can't run script", "No script selected!"); } } /** * @param selection * @return */ private IJavaProject extractJavaProject(ISelection selection) { if (selection instanceof IStructuredSelection) { IStructuredSelection struct = (IStructuredSelection) selection; Object obj = struct.getFirstElement(); if (obj instanceof IAdaptable) { @SuppressWarnings("cast") IJavaProject javaProject = (IJavaProject) ((IAdaptable) obj).getAdapter(IJavaProject.class); if (javaProject != null) { return javaProject; } @SuppressWarnings("cast") IProject project = (IProject) ((IAdaptable) obj).getAdapter(IProject.class); if (project != null) { return JavaCore.create(project); } } } return null; } private ICompilationUnit extractCompilationUnit(ISelection selection) { if (selection instanceof IStructuredSelection) { IStructuredSelection struct = (IStructuredSelection) selection; Object obj = struct.getFirstElement(); if (obj instanceof IAdaptable) { @SuppressWarnings("cast") ICompilationUnit unit = (ICompilationUnit) ((IAdaptable) obj).getAdapter(ICompilationUnit.class); if (unit != null) { return unit; } @SuppressWarnings("cast") IFile file = (IFile) ((IAdaptable) obj).getAdapter(IFile.class); if (file != null) { return JavaCore.createCompilationUnitFrom(file); } } } return null; } /** * Finds or creates a launch configuration for the given file then * launches it. * * @param file The file to launch. * @param mode The mode to launch in. */ protected void launchGroovy(ICompilationUnit unit, IJavaProject javaProject, String mode) { IType runType = null; // if unit is null, then we are not looking for a run type if (unit != null) { IType[] types = null; try { types = unit.getAllTypes(); } catch (JavaModelException e) { GroovyCore.errorRunningGroovy(e); return; } runType = findClassToRun(types); if (runType == null) { GroovyCore.errorRunningGroovy(new Exception(msg)); return; } } Map<String, String> launchConfigProperties = createLaunchProperties(runType, javaProject); try { ILaunchConfigurationWorkingCopy workingConfig = findOrCreateLaunchConfig(launchConfigProperties, runType != null ? runType.getElementName() : javaProject.getElementName()); workingConfig.setAttribute( IJavaLaunchConfigurationConstants.ATTR_CLASSPATH, Arrays.asList( JavaRuntime.computeDefaultRuntimeClassPath(javaProject))); ILaunchConfiguration config = workingConfig.doSave(); DebugUITools.launch(config, mode); } catch (CoreException e) { GroovyCore.errorRunningGroovyFile((IFile) unit.getResource(), e); } } protected abstract String classToRun(); protected Map<String, String> createLaunchProperties(IType runType, IJavaProject javaProject) { Map<String, String> launchConfigProperties = new HashMap<String, String>(); String pathToClass; if (runType != null) { try { pathToClass = " \"${resource_loc:" + runType.getResource().getFullPath().toPortableString() + "}\""; } catch (NullPointerException e) { pathToClass = ""; GroovyCore.errorRunningGroovy(new IllegalArgumentException("Could not find file to run for " + runType)); } } else { pathToClass = ""; } launchConfigProperties.put( IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, "org.codehaus.groovy.tools.GroovyStarter"); launchConfigProperties.put( IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, javaProject.getElementName()); launchConfigProperties.put( IJavaLaunchConfigurationConstants.ATTR_VM_ARGUMENTS, // don't add the groovyConf here // see https://jira.codehaus.org/browse/GRECLIPSE-1650 // "-Dgroovy.starter.conf="+getGroovyConf() + " -Dgroovy.home="+getGroovyHome() ); launchConfigProperties.put( GROOVY_TYPE_TO_RUN, runType == null ? "" : runType.getFullyQualifiedName() ); launchConfigProperties.put(IJavaLaunchConfigurationConstants.ATTR_PROGRAM_ARGUMENTS, "--classpath " + generateClasspath(javaProject) + " --main " + classToRun() + pathToClass); launchConfigProperties.put( IJavaLaunchConfigurationConstants.ATTR_WORKING_DIRECTORY, getWorkingDirectory(runType, javaProject)); return launchConfigProperties; } @SuppressWarnings("unused") private String getGroovyConf() { return "\"${groovy_home}/conf/groovy-starter.conf\""; } private String getGroovyHome() { return "\"${groovy_home}\""; } private String getWorkingDirectory(IType runType, IJavaProject javaProject) { String workingDirSetting = GroovyPlugin.getDefault().getPreferenceStore().getString(PreferenceConstants.GROOVY_SCRIPT_DEFAULT_WORKING_DIRECTORY); if (workingDirSetting.equals(PreferenceConstants.GROOVY_SCRIPT_ECLIPSE_HOME)) { return "${eclipse_home}"; } else if (workingDirSetting.equals(PreferenceConstants.GROOVY_SCRIPT_SCRIPT_LOC) && runType != null) { try { return runType.getResource().getParent().getLocation().toOSString(); } catch (Exception e) { GroovyCore.logException("Exception trying to find the location of " + runType.getElementName(), e); return getProjectLocation(runType); } } else { // (workingDirSetting.equals(PreferenceConstants.GROOVY_SCRIPT_PROJECT_HOME)) // default here if there is no type return getProjectLocation(javaProject); } } private String getProjectLocation(IJavaElement elt) { return "${workspace_loc:" + File.separator + elt.getJavaProject().getProject().getName() + "}"; } private String getProjectLocation(IPath path) { if (path.segmentCount() > 0) { return "${workspace_loc:" + File.separator + path.segment(0) + "}"; } else { return "${workspace_loc}"; } } /* make protected for testing purposes */ protected String generateClasspath(IJavaProject javaProject) { SortedSet<String> sourceEntries = new TreeSet<String>(); SortedSet<String> binEntries = new TreeSet<String>(); addClasspathEntriesForProject(javaProject, sourceEntries, binEntries); StringBuilder sb = new StringBuilder(); sb.append("\""); for (String entry : sourceEntries) { sb.append(entry); sb.append(File.pathSeparator); } for (String entry : binEntries) { sb.append(entry); sb.append(File.pathSeparator); } if (sb.length() > 0) { sb.replace(sb.length()-1, sb.length(), "\""); } return sb.toString(); } /** * Need to recursively walk the classpath and visit all dependent projects * Not looking at classpath containers yet. * * @param javaProject * @param entries */ private void addClasspathEntriesForProject(IJavaProject javaProject, SortedSet<String> sourceEntries, SortedSet<String> binEntries) { List<IJavaProject> dependingProjects = new ArrayList<IJavaProject>(); try { IClasspathEntry[] entries = javaProject.getRawClasspath(); for (IClasspathEntry entry : entries) { int kind = entry.getEntryKind(); switch(kind) { case IClasspathEntry.CPE_LIBRARY: IPath libPath = entry.getPath(); if (!isPathInWorkspace(libPath)) { sourceEntries.add(libPath.toOSString()); break; } //$FALL-THROUGH$ case IClasspathEntry.CPE_SOURCE: IPath srcPath = entry.getPath(); String sloc = getProjectLocation(srcPath); if (srcPath.segmentCount() > 1) { sloc += File.separator + srcPath.removeFirstSegments(1).toOSString(); } sourceEntries.add(sloc); IPath outPath = entry.getOutputLocation(); if (outPath != null) { String bloc = getProjectLocation(outPath); if (outPath.segmentCount() > 1) { bloc += File.separator + outPath.removeFirstSegments(1).toOSString(); } binEntries.add(bloc); } break; case IClasspathEntry.CPE_PROJECT: dependingProjects.add(javaProject.getJavaModel().getJavaProject(entry.getPath().lastSegment())); break; } } IPath defaultOutPath = javaProject.getOutputLocation(); if (defaultOutPath != null) { String bloc = getProjectLocation(javaProject); if (defaultOutPath.segmentCount() > 1) { bloc += File.separator + defaultOutPath.removeFirstSegments(1).toOSString(); } binEntries.add(bloc); } } catch (JavaModelException e) { GroovyCore.logException("Exception generating classpath for launching groovy script", e); } // recur through dependent projects for (IJavaProject dependingProject : dependingProjects) { if (dependingProject.getProject().isAccessible()) { addClasspathEntriesForProject(dependingProject, sourceEntries, binEntries); } } } /** * True if this is a path to a resource in the workspace, false otherwise * * @param libPath * @return */ private boolean isPathInWorkspace(IPath libPath) { if (!libPath.isAbsolute() || libPath.segmentCount() == 0) { return true; } return ResourcesPlugin.getWorkspace().getRoot().getProject(libPath.segment(0)).exists(); } /** * Launches from the source file. * * @see ILaunchShortcut#launch */ public void launch(IEditorPart editor, String mode) { // make sure we are saved as we run groovy from the file editor.getEditorSite().getPage().saveEditor(editor, false); IEditorInput input = editor.getEditorInput(); @SuppressWarnings("cast") IFile file = (IFile) input.getAdapter(IFile.class); ICompilationUnit unit = JavaCore.createCompilationUnitFrom(file); if (unit != null) { launchGroovy(unit, unit.getJavaProject(), mode); } } /** * Finds the runnable classnode in an array. If more than one possible node is found, * will prompt the user to select one. * * @param classNodes * @return Returns a classnode if found, or null if no classNode can be run. * @throws OperationCanceledException If the user selects cancel */ public IType findClassToRun(IType[] types) { IType returnValue = null; List<IType> candidates = new ArrayList<IType>(); for (int i = 0; i < types.length; i++) { if (GroovyProjectFacade.hasRunnableMain(types[i])) { candidates.add(types[i]); } } if (candidates.size() == 1) { returnValue = candidates.get(0); } else { returnValue = LaunchShortcutHelper.chooseClassNode(candidates); } return returnValue; } /** * This method will find a Launch configration that matches the passed * properties of if one is not found will create one. * * @param configProperties A <String, String> Map of launch configuration * properties. * @param classUnderTest The name of the class (without package) that is * being tested. * @return Returns a launch configuration for the class under test with * the passed properties. * @throws CoreException */ public ILaunchConfigurationWorkingCopy findOrCreateLaunchConfig( Map<String, String> configProperties, String simpleMainTypeName) throws CoreException { ILaunchConfigurationWorkingCopy returnConfig; ILaunchConfiguration config = findConfiguration(configProperties.get(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME), configProperties.get(GROOVY_TYPE_TO_RUN)); if (config == null) { returnConfig = createLaunchConfig(configProperties, simpleMainTypeName); } else { returnConfig = config.getWorkingCopy(); } return returnConfig; } /** * This method creates a new launch configuration working copy for the * classUnderTest with the properties defined in configProperites. * * @param configProperties A <String, String> Map of launch configuration * properties. * @param classUnderTest The name of the class (without package) that is * being tested. * @return Returns a new launch configuration. * @throws CoreException */ public ILaunchConfigurationWorkingCopy createLaunchConfig(Map<String, String> configProperties, String classUnderTest) throws CoreException { String launchName = getLaunchManager().generateLaunchConfigurationName(classUnderTest); ILaunchConfigurationWorkingCopy returnConfig = getGroovyLaunchConfigType().newInstance(null, launchName); for (Iterator<String> it = configProperties.keySet().iterator(); it.hasNext();) { String key = it.next(); String value = configProperties.get(key); returnConfig.setAttribute(key, value); } return returnConfig; } /** * This class finds any launch configrations that match the defined * properties. If more that one match is found the user is prompted * to select one. * * Semantics now matches {@link JavaLaunchShortcut}. If the main type name * and the * project name match, then this is considered a match. * * @param configProperties A <String, String> Map of properties to check * when searching for a matching launch configuration. * @return Returns a launch configuration that matches the given properties * if a match is found, otherwise returns null. * @throws CoreException */ private ILaunchConfiguration findConfiguration(String projectName, String mainTypeName) throws CoreException { ILaunchConfiguration returnValue = null; ILaunchConfigurationType configType = getGroovyLaunchConfigType(); List<ILaunchConfiguration> candidateConfigs = ListUtil.newEmptyList(); ILaunchConfiguration[] configs = getLaunchManager().getLaunchConfigurations(configType); for (int i = 0; i < configs.length; i++) { ILaunchConfiguration config = configs[i]; if (config.getAttribute(GROOVY_TYPE_TO_RUN, "").equals(mainTypeName) && config.getAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, "").equals(projectName)) { candidateConfigs.add(config); } } int candidateCount = candidateConfigs.size(); if (candidateCount == 1) { returnValue = candidateConfigs.get(0); } else if (candidateCount > 1) { returnValue = chooseConfiguration(candidateConfigs); } return returnValue; } /** * Prompts the user to select a launch configuration from configList. * * @param configList A List of ILaunchConfigrations for the user to * pick from. * @return Returns the ILaunchConfiguration that the user selected. */ public ILaunchConfiguration chooseConfiguration(List< ILaunchConfiguration > configList) { IDebugModelPresentation labelProvider = DebugUITools.newDebugModelPresentation(); return LaunchShortcutHelper.chooseFromList(configList, labelProvider, title, text); } /** * This is a convenience method for getting the Groovy launch configuration * type from the Launch Manager. * * @return Returns the ILaunchConfigurationType for running Groovy classes. */ protected abstract ILaunchConfigurationType getGroovyLaunchConfigType(); /** * This is a convenince method for getting the Launch Manager from * the Debug plugin. * * @return Returns the default Eclipse launch manager. */ public static ILaunchManager getLaunchManager() { return DebugPlugin.getDefault().getLaunchManager(); } protected abstract String applicationOrConsole(); protected abstract boolean canLaunchWithNoType(); }