/******************************************************************************* * Copyright (c) 2013, 2016 Pivotal Software, 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 Software, Inc. - initial API and implementation *******************************************************************************/ package org.springframework.ide.eclipse.boot.core.internal; import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.ARTIFACT_ID; import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.CLASSIFIER; import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.DEPENDENCIES; import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.DEPENDENCY; import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.DEPENDENCY_MANAGEMENT; import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.GROUP_ID; import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.ID; import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.NAME; import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.OPTIONAL; import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.SCOPE; import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.TYPE; import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.URL; import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.VERSION; import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.childEquals; import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.createElement; import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.createElementWithText; import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.findChild; import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.findChilds; import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.format; import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.getChild; import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.getTextValue; import static org.eclipse.m2e.core.ui.internal.editing.PomEdits.performOnDOMDocument; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutionException; import org.apache.commons.lang3.StringUtils; import org.apache.maven.model.Dependency; import org.apache.maven.model.DependencyManagement; import org.apache.maven.project.MavenProject; import org.eclipse.core.resources.IContainer; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.SubMonitor; import org.eclipse.core.runtime.jobs.Job; 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.ui.IDebugUIConstants; import org.eclipse.debug.ui.RefreshTab; import org.eclipse.jdt.core.IClasspathEntry; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; import org.eclipse.jdt.launching.JavaRuntime; import org.eclipse.m2e.actions.MavenLaunchConstants; import org.eclipse.m2e.core.MavenPlugin; import org.eclipse.m2e.core.internal.IMavenConstants; import org.eclipse.m2e.core.project.IMavenProjectFacade; import org.eclipse.m2e.core.project.IMavenProjectRegistry; import org.eclipse.m2e.core.project.ResolverConfiguration; import org.eclipse.m2e.core.ui.internal.UpdateMavenProjectJob; import org.eclipse.m2e.core.ui.internal.editing.PomEdits; import org.eclipse.m2e.core.ui.internal.editing.PomEdits.Operation; import org.eclipse.m2e.core.ui.internal.editing.PomEdits.OperationTuple; import org.eclipse.m2e.core.ui.internal.editing.PomHelper; import org.springframework.ide.eclipse.boot.core.Bom; import org.springframework.ide.eclipse.boot.core.BootActivator; import org.springframework.ide.eclipse.boot.core.IMavenCoordinates; import org.springframework.ide.eclipse.boot.core.MavenCoordinates; import org.springframework.ide.eclipse.boot.core.MavenId; import org.springframework.ide.eclipse.boot.core.Repo; import org.springframework.ide.eclipse.boot.core.SpringBootCore; import org.springframework.ide.eclipse.boot.core.SpringBootStarter; import org.springframework.ide.eclipse.boot.core.initializr.InitializrService; import org.springframework.ide.eclipse.boot.util.DumpOutput; import org.springframework.ide.eclipse.boot.util.Log; import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; import org.springsource.ide.eclipse.commons.ui.launch.LaunchUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; /** * @author Kris De Volder */ @SuppressWarnings("restriction") public class MavenSpringBootProject extends SpringBootProject { /** * Debug flag, may be flipped on temporarily by test code to spy on the output of maven * execution. */ public static boolean DUMP_MAVEN_OUTPUT = false; private static final String MVN_LAUNCH_MODE = "run"; //TODO: properly handle pom manipulation when pom file is open / dirty in an editor. // minimum requirement: detect and prohibit by throwing an error. private static final boolean DEBUG = (""+Platform.getLocation()).contains("kdvolder"); private static final String REPOSITORIES = "repositories"; private static final String REPOSITORY = "repository"; private static final String SNAPSHOTS = "snapshots"; private static final String ENABLED = "enabled"; public MavenSpringBootProject(IProject project, InitializrService initializr) { super(project, initializr); } @Override public IProject getProject() { return project; } private MavenProject getMavenProject() throws CoreException { IMavenProjectFacade mpf = getMavenProjectFacade(); if (mpf!=null) { return mpf.getMavenProject(new NullProgressMonitor()); } return null; } private IMavenProjectFacade getMavenProjectFacade() { IMavenProjectRegistry pr = MavenPlugin.getMavenProjectRegistry(); IMavenProjectFacade mpf = pr.getProject(project); return mpf; } private IFile getPomFile() { return project.getFile(new Path("pom.xml")); } @Override public List<IMavenCoordinates> getDependencies() throws CoreException { MavenProject mp = getMavenProject(); if (mp!=null) { return toMavenCoordinates(mp.getDependencies()); } return Collections.emptyList(); } private List<IMavenCoordinates> toMavenCoordinates(List<Dependency> dependencies) { ArrayList<IMavenCoordinates> converted = new ArrayList<>(dependencies.size()); for (Dependency d : dependencies) { converted.add(new MavenCoordinates(d.getGroupId(), d.getArtifactId(), d.getClassifier(), d.getVersion())); } return converted; } /** * Determine the 'managed' version, if any, associate with a given dependency. * @return Version string or null. */ private String getManagedVersion(IMavenCoordinates dep) { try { MavenProject mp = getMavenProject(); if (mp!=null) { DependencyManagement managedDeps = mp.getDependencyManagement(); if (managedDeps!=null) { List<Dependency> deps = managedDeps.getDependencies(); if (deps!=null && !deps.isEmpty()) { for (Dependency d : deps) { if ("jar".equals(d.getType())) { if (dep.getArtifactId().equals(d.getArtifactId()) && dep.getGroupId().equals(d.getGroupId())) { return d.getVersion(); } } } } } } } catch (Exception e) { BootActivator.log(e); } return null; } private void debug(String string) { if (DEBUG) { System.out.println(string); } } @Override public void addMavenDependency(final IMavenCoordinates dep, final boolean preferManagedVersion) throws CoreException { addMavenDependency(dep, preferManagedVersion, false); } @Override public void addMavenDependency( final IMavenCoordinates dep, final boolean preferManagedVersion, final boolean optional ) throws CoreException { try { IFile file = getPomFile(); performOnDOMDocument(new OperationTuple(file, new Operation() { public void process(Document document) { Element depsEl = getChild( document.getDocumentElement(), DEPENDENCIES); if (depsEl==null) { //TODO: handle this case } else { String version = dep.getVersion(); String managedVersion = getManagedVersion(dep); if (managedVersion!=null) { //Decide whether we can/should inherit the managed version or override it. if (preferManagedVersion || managedVersion.equals(version)) { version = null; } } else { //No managed version. We have to include a version in xml added to the pom. } Element xmlDep = PomHelper.createDependency(depsEl, dep.getGroupId(), dep.getArtifactId(), version ); if (optional) { createElementWithText(xmlDep, OPTIONAL, "true"); format(xmlDep); } } } })); } catch (Throwable e) { throw ExceptionUtil.coreException(e); } } @Override public void setStarters(Collection<SpringBootStarter> _starters) throws CoreException { try { final Set<MavenId> starters = new HashSet<>(); for (SpringBootStarter s : _starters) { starters.add(s.getMavenId()); } IFile file = getPomFile(); performOnDOMDocument(new OperationTuple(file, new Operation() { public void process(Document pom) { Element depsEl = getChild( pom.getDocumentElement(), DEPENDENCIES); List<Element> children = findChilds(depsEl, DEPENDENCY); for (Element c : children) { //We only care about 'starter' dependencies. Leave everything else alone. // Also... don't touch nodes that are already there, unless they are to // be removed. This way we don't mess up versions, comments or other stuff // that a user may have inserted via manual edits. String aid = getTextValue(findChild(c, ARTIFACT_ID)); String gid = getTextValue(findChild(c, GROUP_ID)); if (aid!=null && gid!=null) { //ignore invalid entries that don't have gid or aid if (isKnownStarter(new MavenId(gid, aid))) { MavenId id = new MavenId(gid, aid); boolean keep = starters.remove(id); if (!keep) { depsEl.removeChild(c); } } } } //if 'starters' is not empty at this point, it contains remaining ids we have not seen // in the pom, so we need to add them. for (MavenId mid : starters) { SpringBootStarter starter = getStarter(mid); createDependency(depsEl, starter.getDependency(), starter.getScope()); createBomIfNeeded(pom, starter.getBom()); createRepoIfNeeded(pom, starter.getRepo()); } } })); } catch (Throwable e) { throw ExceptionUtil.coreException(e); } } @Override public void removeMavenDependency(final MavenId mavenId) { IFile file = getPomFile(); try { performOnDOMDocument(new OperationTuple(file, new Operation() { @Override public void process(Document pom) { Element depsEl = getChild( pom.getDocumentElement(), DEPENDENCIES); if (depsEl!=null) { Element dep = findChild(depsEl, DEPENDENCY, childEquals(GROUP_ID, mavenId.getGroupId()), childEquals(ARTIFACT_ID, mavenId.getArtifactId()) ); if (dep!=null) { depsEl.removeChild(dep); } } } })); } catch (Exception e) { BootActivator.log(e); } } @Override public Job updateProjectConfiguration() { Job job = new UpdateMavenProjectJob(new IProject[] { getProject() }); job.schedule(); return job; } @Override public String getBootVersion() { try { MavenProject mp = getMavenProject(); if (mp!=null) { return getBootVersion(mp.getDependencies()); } } catch (Exception e) { BootActivator.log(e); } return SpringBootCore.getDefaultBootVersion(); } private String getBootVersion(List<Dependency> dependencies) { for (Dependency dep : dependencies) { if (dep.getArtifactId().startsWith("spring-boot") && dep.getGroupId().equals("org.springframework.boot")) { return dep.getVersion(); } } return SpringBootCore.getDefaultBootVersion(); } private void createRepoIfNeeded(Document pom, Repo repo) { if (repo!=null) { addReposIfNeeded(pom, Collections.singletonList(repo)); } } private void createBomIfNeeded(Document pom, Bom bom) { if (bom!=null) { Element bomList = ensureDependencyMgmtSection(pom); Element existing = PomEdits.findChild(bomList, DEPENDENCY, childEquals(GROUP_ID, bom.getGroupId()), childEquals(ARTIFACT_ID, bom.getArtifactId()) ); if (existing==null) { createBom(bomList, bom); addReposIfNeeded(pom, bom.getRepos()); } } } private Element ensureDependencyMgmtSection(Document pom) { /* Ensure that this exists in the pom: <dependencyManagement> <dependencies> <---- RETURNED <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-parent</artifactId> <version>Brixton.M3</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> */ boolean needFormatting = false; Element doc = pom.getDocumentElement(); Element depman = findChild(doc, DEPENDENCY_MANAGEMENT); if (depman==null) { depman = createElement(doc, DEPENDENCY_MANAGEMENT); needFormatting = true; } Element deplist = findChild(depman, DEPENDENCIES); if (deplist==null) { deplist = createElement(depman, DEPENDENCIES); } if (needFormatting) { format(depman); } return deplist; } private static Element createBom(Element parentList, Bom bom) { /* <dependencyManagement> <dependencies> <---- parentList <dependency> <---- create and return <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-parent</artifactId> <version>Brixton.M3</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> */ String groupId = bom.getGroupId(); String artifactId = bom.getArtifactId(); String version = bom.getVersion(); String classifier = bom.getClassifier(); String type = "pom"; String scope = "import"; Element dep = createElement(parentList, DEPENDENCY); if(groupId != null) { createElementWithText(dep, GROUP_ID, groupId); } createElementWithText(dep, ARTIFACT_ID, artifactId); if(version != null) { createElementWithText(dep, VERSION, version); } createElementWithText(dep, TYPE, type); if (scope !=null && !scope.equals("compile")) { createElementWithText(dep, SCOPE, scope); } if (classifier!=null) { createElementWithText(dep, CLASSIFIER, classifier); } format(dep); return dep; } /** * creates and adds new dependency to the parent. formats the result. */ private Element createDependency(Element parentList, IMavenCoordinates info, String scope) { Element dep = createElement(parentList, DEPENDENCY); String groupId = info.getGroupId(); String artifactId = info.getArtifactId(); String version = info.getVersion(); String classifier = info.getClassifier(); if(groupId != null) { createElementWithText(dep, GROUP_ID, groupId); } createElementWithText(dep, ARTIFACT_ID, artifactId); if(version != null) { createElementWithText(dep, VERSION, version); } if (classifier != null) { createElementWithText(dep, CLASSIFIER, classifier); } if (scope!=null && !scope.equals("compile")) { createElementWithText(dep, SCOPE, scope); } format(dep); return dep; } private void addReposIfNeeded(Document pom, List<Repo> repos) { //Example: // <repositories> // <repository> // <id>spring-snapshots</id> // <name>Spring Snapshots</name> // <url>https://repo.spring.io/snapshot</url> // <snapshots> // <enabled>true</enabled> // </snapshots> // </repository> // <repository> // <id>spring-milestones</id> // <name>Spring Milestones</name> // <url>https://repo.spring.io/milestone</url> // <snapshots> // <enabled>false</enabled> // </snapshots> // </repository> // </repositories> if (repos!=null && !repos.isEmpty()) { Element doc = pom.getDocumentElement(); Element repoList = findChild(doc, REPOSITORIES); if (repoList==null) { repoList = createElement(doc, REPOSITORIES); format(repoList); } for (Repo repo : repos) { String id = repo.getId(); Element repoEl = findChild(repoList, REPOSITORY, childEquals(ID, id)); if (repoEl==null) { repoEl = createElement(repoList, REPOSITORY); createElementWithTextMaybe(repoEl, ID, id); createElementWithTextMaybe(repoEl, NAME, repo.getName()); createElementWithTextMaybe(repoEl, URL, repo.getUrl()); Boolean isSnapshot = repo.getSnapshotEnabled(); if (isSnapshot!=null) { Element snapshot = createElement(repoEl, SNAPSHOTS); createElementWithText(snapshot, ENABLED, isSnapshot.toString()); } format(repoEl); } } } } private void createElementWithTextMaybe(Element parent, String name, String text) { if (StringUtils.isNotBlank(text)) { createElementWithText(parent, name, text); } } @Override public String getDependencyFileName() { return "pom.xml"; } @Override public String getPackaging() throws CoreException { MavenProject mp = getMavenProject(); if (mp!=null) { return mp.getPackaging(); } return null; } @Override public File executePackagingScript(IProgressMonitor monitor) throws CoreException { monitor.beginTask("Building War file", 100); try { ILaunchConfiguration launchConf = createLaunchConfiguration(project, "package"); ILaunch launch = launchConf.launch(MVN_LAUNCH_MODE, SubMonitor.convert(monitor, 10), true, true); if (DUMP_MAVEN_OUTPUT) { launch.getProcesses()[0].getStreamsProxy().getOutputStreamMonitor().addListener(new DumpOutput("%mvn-out")); launch.getProcesses()[0].getStreamsProxy().getErrorStreamMonitor().addListener(new DumpOutput("%mvn-err")); } LaunchUtils.whenTerminated(launch).get(); int exitValue = launch.getProcesses()[0].getExitValue(); if (exitValue!=0) { throw ExceptionUtil.coreException("Non-zero exit-code("+exitValue+") from maven war packaging. Check maven console for errors!"); } return findWarFile(); } catch (ExecutionException | InterruptedException e) { throw ExceptionUtil.coreException(e); } finally { monitor.done(); } } private File findWarFile() throws CoreException { File warFile = getWarFile(); if (warFile==null) { throw ExceptionUtil.coreException("Couldn't determine where to find the war file after 'mvn package'"); } else if (!warFile.isFile()) { throw ExceptionUtil.coreException("Couldn't find file to deploy at '"+warFile+"' after running 'mvn package'"); } return warFile; } private File getWarFile() throws CoreException { MavenProject mpf = getMavenProject(); if (mpf!=null) { String buildDir = mpf.getBuild().getDirectory(); String fName = mpf.getBuild().getFinalName(); String type = mpf.getPackaging(); if (buildDir!=null && fName!=null && type!=null) { return new File(new File(buildDir), fName+"."+type); } } return null; } /////////////////////////////////////////////////////////////////////////////////////// /// m2e cruft... copied from hither and tither. private ILaunchConfiguration createLaunchConfiguration(IContainer basedir, String goal) { try { ILaunchManager launchManager = DebugPlugin.getDefault().getLaunchManager(); ILaunchConfigurationType launchConfigurationType = launchManager .getLaunchConfigurationType(MavenLaunchConstants.LAUNCH_CONFIGURATION_TYPE_ID); String rawConfigName = basedir.getName()+"-build-war"; String safeConfigName = launchManager.generateLaunchConfigurationName(rawConfigName); ILaunchConfigurationWorkingCopy workingCopy = launchConfigurationType.newInstance(null, safeConfigName); workingCopy.setAttribute(MavenLaunchConstants.ATTR_POM_DIR, basedir.getLocation().toOSString()); workingCopy.setAttribute(MavenLaunchConstants.ATTR_GOALS, goal); workingCopy.setAttribute(MavenLaunchConstants.ATTR_SKIP_TESTS, true); workingCopy.setAttribute(IDebugUIConstants.ATTR_PRIVATE, true); workingCopy.setAttribute(RefreshTab.ATTR_REFRESH_SCOPE, "${project}"); //$NON-NLS-1$ workingCopy.setAttribute(RefreshTab.ATTR_REFRESH_RECURSIVE, true); setProjectConfiguration(workingCopy, basedir); IPath path = getJREContainerPath(basedir); if(path != null) { workingCopy.setAttribute(IJavaLaunchConfigurationConstants.ATTR_JRE_CONTAINER_PATH, path.toPortableString()); } // TODO when launching Maven with debugger consider to add the following property // -Dmaven.surefire.debug="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -Xnoagent -Djava.compiler=NONE" return workingCopy; } catch(CoreException ex) { Log.log(ex); } return null; } // TODO ideally it should use MavenProject, but it is faster to scan IJavaProjects private IPath getJREContainerPath(IContainer basedir) throws CoreException { IProject project = basedir.getProject(); if(project != null && project.hasNature(JavaCore.NATURE_ID)) { IJavaProject javaProject = JavaCore.create(project); IClasspathEntry[] entries = javaProject.getRawClasspath(); for(int i = 0; i < entries.length; i++ ) { IClasspathEntry entry = entries[i]; if(JavaRuntime.JRE_CONTAINER.equals(entry.getPath().segment(0))) { return entry.getPath(); } } } return null; } private void setProjectConfiguration(ILaunchConfigurationWorkingCopy workingCopy, IContainer basedir) { IMavenProjectRegistry projectManager = MavenPlugin.getMavenProjectRegistry(); IFile pomFile = basedir.getFile(new Path(IMavenConstants.POM_FILE_NAME)); IMavenProjectFacade projectFacade = projectManager.create(pomFile, false, new NullProgressMonitor()); if(projectFacade != null) { ResolverConfiguration configuration = projectFacade.getResolverConfiguration(); String selectedProfiles = configuration.getSelectedProfiles(); if(selectedProfiles != null && selectedProfiles.length() > 0) { workingCopy.setAttribute(MavenLaunchConstants.ATTR_PROFILES, selectedProfiles); } } } }