/******************************************************************************* * Copyright (c) 2015-2016 Pivotal, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Pivotal, Inc. - initial API and implementation *******************************************************************************/ package org.springframework.ide.eclipse.boot.launch; import static org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.debug.core.DebugPlugin; import org.eclipse.debug.core.ILaunch; 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.core.model.IDebugTarget; import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; import org.eclipse.jdt.launching.JavaLaunchDelegate; import org.springframework.ide.eclipse.boot.core.BootActivator; import org.springframework.ide.eclipse.boot.core.BootPreferences; import org.springframework.ide.eclipse.boot.core.SpringBootCore; import org.springframework.ide.eclipse.boot.util.Log; import org.springframework.ide.eclipse.boot.util.ProcessListenerAdapter; import org.springframework.ide.eclipse.boot.util.ProcessTracker; import org.springframework.ide.eclipse.editor.support.util.StringUtil; public abstract class AbstractBootLaunchConfigurationDelegate extends JavaLaunchDelegate { private static final String SILENT_EXIT_EXCEPTION = "org.springframework.boot.devtools.restart.SilentExitExceptionHandler$SilentExitException"; private static final String M2E_CLASSPATH_PROVIDER = "org.eclipse.m2e.launchconfig.classpathProvider"; protected static final String M2E_SOURCEPATH_PROVIDER = "org.eclipse.m2e.launchconfig.sourcepathProvider"; public static final String JAVA_LAUNCH_CONFIG_TYPE_ID = IJavaLaunchConfigurationConstants.ID_JAVA_APPLICATION; public static final String ENABLE_DEBUG_OUTPUT = "spring.boot.debug.enable"; public static final boolean DEFAULT_ENABLE_DEBUG_OUTPUT = false; private static final String BOOT_MAVEN_SOURCE_PATH_PROVIDER = "org.springframework.ide.eclipse.boot.launch.BootMavenSourcePathProvider"; /** * Spring boot properties are stored as launch confiuration properties with * an extra prefix added to property name to avoid name clashes with * other launch config properties. */ private static final String PROPS_PREFIX = "spring.boot.prop."; /** * To be able to store multiple assignment to the same spring boot * property name we add a 'oid' at the end of each stored * property name. ?_SEPERATOR is used to separate the 'real' * property name from the 'oid' string. */ private static final char OID_SEPERATOR = ':'; public static class PropVal { public String name; public String value; public boolean isChecked; public PropVal(String name, String value, boolean isChecked) { //Don't use null, use empty Strings Assert.isNotNull(name); Assert.isNotNull(value); this.name = name; this.value = value; this.isChecked = isChecked; } @Override public String toString() { return (isChecked?"[X] ":"[ ] ") + name + "="+ value; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + (isChecked ? 1231 : 1237); result = prime * result + ((name == null) ? 0 : name.hashCode()); result = prime * result + ((value == null) ? 0 : value.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; PropVal other = (PropVal) obj; if (isChecked != other.isChecked) return false; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; if (value == null) { if (other.value != null) return false; } else if (!value.equals(other.value)) return false; return true; } } @Override public String[] getClasspath(ILaunchConfiguration conf) throws CoreException { try { //Must do exactly what a Java Launch config would do. It is not enough to simply // call super. Me must also pass a launch config exactly like the JDT one, including // its type and some 'magic' attributes added for m2e ILaunchConfigurationWorkingCopy wc = copyAs(conf, JAVA_LAUNCH_CONFIG_TYPE_ID); enableMavenClasspathProvider(wc); return super.getClasspath(wc); } catch (Exception e) { //In case the hacky stuff above fails, do something that mostly works, even if // it does gets a classpath polluted with test dependencies. // See https://issuetracker.springsource.com/browse/STS-4085 BootActivator.log(e); return super.getClasspath(conf); } } public static List<ILaunchConfiguration> getLaunchConfigs(IProject p, String confTypeId) { try { ILaunchManager lm = getLaunchMan(); ILaunchConfigurationType type = lm.getLaunchConfigurationType(confTypeId); if (type!=null) { ILaunchConfiguration[] configs = lm.getLaunchConfigurations(type); if (configs!=null && configs.length>0) { ArrayList<ILaunchConfiguration> result = new ArrayList<>(); for (ILaunchConfiguration conf : configs) { if (p.equals(getProject(conf))) { result.add(conf); } } return result; } } } catch (Exception e) { BootActivator.log(e); } return Collections.emptyList(); } public static void clearProperties(ILaunchConfigurationWorkingCopy conf) { try { //note: e43 doesn't use generics for conf.getAttributes, hence the // funky casting below. for (Object _prefixedProp : conf.getAttributes().keySet()) { String prefixedProp = (String) _prefixedProp; if (prefixedProp.startsWith(PROPS_PREFIX)) { conf.removeAttribute(prefixedProp); } } } catch (Exception e) { BootActivator.log(e); } } public static String getMainType(ILaunchConfiguration config) throws CoreException { return config.getAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, (String)null); } public static void setMainType(ILaunchConfigurationWorkingCopy config, String typeName) { config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, typeName); } @SuppressWarnings("unchecked") public static List<PropVal> getProperties(ILaunchConfiguration conf) { ArrayList<PropVal> props = new ArrayList<>(); try { //Note: in e43 conf.getAttributes doesn't use generics yet. So to //build with 4.3 we need to to some funky casting below. for (Object _e : conf.getAttributes().entrySet()) { try { Map.Entry<String, Object> e = (Entry<String, Object>) _e; String prefixed = e.getKey(); if (prefixed.startsWith(PROPS_PREFIX)) { String name = prefixed.substring(PROPS_PREFIX.length()); int dotPos = name.lastIndexOf(OID_SEPERATOR); if (dotPos>=0) { name = name.substring(0, dotPos); } String valueEnablement = (String)e.getValue(); String value = valueEnablement.substring(1); boolean enabled = valueEnablement.charAt(0)=='1'; props.add(new PropVal(name, value, enabled)); } } catch (Exception ignore) { //silently ignore invalid property data. } } } catch (Exception e) { BootActivator.log(e); } return props; } public static void setProperties(ILaunchConfigurationWorkingCopy conf, List<PropVal> props) { if (props==null) { props = Collections.emptyList(); } clearProperties(conf); int oid = 0; //unique id appended to each stored key, otherwise we loose //entries with identical keys. for (PropVal p : props) { //Don't store stuff with 'empty keys'. These are likely just // 'empty' entries user added but never filled in. if (StringUtil.hasText(p.name)) { String prefixed = PROPS_PREFIX+p.name+OID_SEPERATOR+(oid++); String valueEnabled = (p.isChecked?'1':'0')+p.value; conf.setAttribute(prefixed, valueEnabled); } } } protected void addPropertiesArguments(ArrayList<String> args, List<PropVal> props) { for (PropVal p : props) { //spring boot doesn't like empty option keys/values so skip those. if (p.isChecked && !p.name.isEmpty() && !p.value.isEmpty()) { args.add(propertyAssignmentArgument(p.name, p.value)); } } } protected String propertyAssignmentArgument(String name, String value) { if (name.contains("=")) { //spring boot has no handling of escape sequences like '\=' //so we cannot represent keys containing '='. throw new IllegalArgumentException("property name shouldn't contain '=':"+name); } return "--"+name + "=" +value; } public static boolean getEnableDebugOutput(ILaunchConfiguration conf) { try { return conf.getAttribute(ENABLE_DEBUG_OUTPUT, DEFAULT_ENABLE_DEBUG_OUTPUT); } catch (Exception e) { BootActivator.log(e); return DEFAULT_ENABLE_DEBUG_OUTPUT; } } public static void setEnableDebugOutput(ILaunchConfigurationWorkingCopy conf, boolean enable) { conf.setAttribute(ENABLE_DEBUG_OUTPUT, enable); } /** * Get the project associated with this a luanch config. Note that this * method returns an IProject reference regardless of whether or not the * project exists. */ public static IProject getProject(ILaunchConfiguration conf) { try { String pname = getProjectName(conf); if (StringUtil.hasText(pname)) { IProject p = ResourcesPlugin.getWorkspace().getRoot().getProject(pname); //debug(conf, "getProject => "+p); return p; } } catch (Exception e) { Log.log(e); } //debug(conf, "getProject => NULL"); return null; } public static String getProjectName(ILaunchConfiguration conf) throws CoreException { return conf.getAttribute(ATTR_PROJECT_NAME, ""); } public static void setProject(ILaunchConfigurationWorkingCopy conf, IProject p) { //debug(conf, "setProject <= "+p); if (p==null) { conf.removeAttribute(ATTR_PROJECT_NAME); } else { conf.setAttribute(ATTR_PROJECT_NAME, p.getName()); } } /** * Enable maven classpath provider if applicable to this conf. * Addresses https://issuetracker.springsource.com/browse/STS-4085 */ static void enableMavenClasspathProvider(ILaunchConfigurationWorkingCopy conf) { try { if (conf.getType().getIdentifier().equals(JAVA_LAUNCH_CONFIG_TYPE_ID)) { //Take care not to add this a 'real' Boot launch config or it will cause m2e to throw exceptions //These 'magic' attributes should only be added to a 'cloned' copy of our config with the right type. IProject p = getProject(conf); if (p!=null && p.hasNature(SpringBootCore.M2E_NATURE)) { if (!conf.hasAttribute(IJavaLaunchConfigurationConstants.ATTR_CLASSPATH_PROVIDER)) { conf.setAttribute(IJavaLaunchConfigurationConstants.ATTR_CLASSPATH_PROVIDER, M2E_CLASSPATH_PROVIDER); } if (!conf.hasAttribute(IJavaLaunchConfigurationConstants.ATTR_SOURCE_PATH_PROVIDER)) { conf.setAttribute(IJavaLaunchConfigurationConstants.ATTR_SOURCE_PATH_PROVIDER, M2E_SOURCEPATH_PROVIDER); } } } } catch (Exception e) { BootActivator.log(e); } } /** * Copy a given launch config into a 'clone' that has all the same attributes but * a different type id. * @throws CoreException */ private static ILaunchConfigurationWorkingCopy copyAs(ILaunchConfiguration conf, String newType) throws CoreException { ILaunchManager launchManager = getLaunchMan(); ILaunchConfigurationType launchConfigurationType = launchManager .getLaunchConfigurationType(newType); ILaunchConfigurationWorkingCopy wc = launchConfigurationType.newInstance(null, launchManager.generateLaunchConfigurationName(conf.getName())); wc.setAttributes(conf.getAttributes()); return wc; } public static ILaunchManager getLaunchMan() { return DebugPlugin.getDefault().getLaunchManager(); } @Override public void launch(ILaunchConfiguration conf, String mode, final ILaunch launch, IProgressMonitor monitor) throws CoreException { conf = configureSourcePathProvider(conf); if (ILaunchManager.DEBUG_MODE.equals(mode) && isIgnoreSilentExitException(conf)) { final IgnoreExceptionOfType breakpointListener = new IgnoreExceptionOfType(launch, SILENT_EXIT_EXCEPTION); new ProcessTracker(new ProcessListenerAdapter() { @Override public void debugTargetTerminated(ProcessTracker tracker, IDebugTarget target) { if (launch.equals(target.getLaunch())){ breakpointListener.dispose(); tracker.dispose(); } } }); } super.launch(conf, mode, launch, monitor); } protected ILaunchConfiguration configureSourcePathProvider(ILaunchConfiguration conf) throws CoreException { IProject project = BootLaunchConfigurationDelegate.getProject(conf); if (project.hasNature(SpringBootCore.M2E_NATURE)) { conf = setAttribute(conf, IJavaLaunchConfigurationConstants.ATTR_SOURCE_PATH_PROVIDER, BOOT_MAVEN_SOURCE_PATH_PROVIDER); } return conf; } private ILaunchConfiguration setAttribute(ILaunchConfiguration conf, String a, String v) { try { if (!Objects.equals(v, conf.getAttribute(a, (String)null))) { ILaunchConfigurationWorkingCopy wc = conf.getWorkingCopy(); wc.setAttribute(a, v); conf = wc.doSave(); } } catch (Exception e) { BootActivator.log(e); } return conf; } public static boolean isIgnoreSilentExitException(ILaunchConfiguration conf) { //This might be controlled by individual launch conf in future, but for now, it is just a global preference. return BootPreferences.getInstance().isIgnoreSilentExit(); } }