From b86c44622ebbff73b46715790bf791e57674d589 Mon Sep 17 00:00:00 2001 From: Victor Rubezhny Date: Fri, 26 Aug 2022 15:39:59 +0200 Subject: [PATCH] Add Managed Version Info on hover #238 Signed-off-by: Victor Rubezhny --- .../hover/MavenHoverParticipant.java | 211 +++++++++++++++++- .../extensions/maven/utils/MarkdownUtils.java | 17 ++ .../hover/ManagedVersionHoverTest.java | 116 ++++++++++ 3 files changed, 334 insertions(+), 10 deletions(-) create mode 100644 lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/participants/hover/ManagedVersionHoverTest.java diff --git a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/hover/MavenHoverParticipant.java b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/hover/MavenHoverParticipant.java index 77e0a383..aa626a10 100644 --- a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/hover/MavenHoverParticipant.java +++ b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/hover/MavenHoverParticipant.java @@ -12,8 +12,18 @@ import static org.eclipse.lemminx.extensions.maven.DOMConstants.CONFIGURATION_ELT; import static org.eclipse.lemminx.extensions.maven.DOMConstants.GOAL_ELT; import static org.eclipse.lemminx.extensions.maven.DOMConstants.GROUP_ID_ELT; +import static org.eclipse.lemminx.extensions.maven.DOMConstants.PROJECT_ELT; +import static org.eclipse.lemminx.extensions.maven.DOMConstants.PARENT_ELT; +import static org.eclipse.lemminx.extensions.maven.DOMConstants.DEPENDENCIES_ELT; +import static org.eclipse.lemminx.extensions.maven.DOMConstants.DEPENDENCY_ELT; +import static org.eclipse.lemminx.extensions.maven.DOMConstants.PLUGINS_ELT; +import static org.eclipse.lemminx.extensions.maven.DOMConstants.PLUGIN_ELT; +import static org.eclipse.lemminx.extensions.maven.DOMConstants.RELATIVE_PATH_ELT; import static org.eclipse.lemminx.extensions.maven.DOMConstants.VERSION_ELT; +import java.io.File; +import java.net.URI; +import java.text.MessageFormat; import java.util.Comparator; import java.util.List; import java.util.Map; @@ -25,8 +35,13 @@ import java.util.logging.Logger; import java.util.stream.Collectors; +import org.apache.maven.Maven; import org.apache.maven.artifact.versioning.DefaultArtifactVersion; import org.apache.maven.model.Dependency; +import org.apache.maven.model.InputLocation; +import org.apache.maven.model.InputSource; +import org.apache.maven.model.Model; +import org.apache.maven.model.Parent; import org.apache.maven.model.building.ModelBuilder; import org.apache.maven.model.building.ModelBuildingRequest; import org.apache.maven.plugin.InvalidPluginDescriptorException; @@ -100,7 +115,6 @@ public Hover onText(IHoverRequest request) throws Exception { DOMNode tag = request.getNode(); DOMElement parent = tag.getParentElement(); - boolean isPlugin = ParticipantUtils.isPlugin(parent); boolean isParentDeclaration = ParticipantUtils.isParentDeclaration(parent); Map.Entry mavenProperty = ParticipantUtils.getMavenPropertyInRequest(request); @@ -115,16 +129,19 @@ public Hover onText(IHoverRequest request) throws Exception { case GROUP_ID_ELT: case ARTIFACT_ID_ELT: case VERSION_ELT: - Hover hover = isParentDeclaration && p != null && p.getParent() != null ? - hoverForProject(request.canSupportMarkupKind(MarkupKind.MARKDOWN), p.getParent()) : null; + Hover hover = isParentDeclaration && p != null && p.getParent() != null ? hoverForProject(request, + p.getParent(), ParticipantUtils.isWellDefinedDependency(artifactToSearch)) : null; if (hover == null) { Artifact artifact = ParticipantUtils.findWorkspaceArtifact(plugin, request, artifactToSearch); - if (artifact != null && artifact.getFile() != null) { - return hoverForProject(request.canSupportMarkupKind(MarkupKind.MARKDOWN), plugin.getProjectCache().getSnapshotProject(artifact.getFile()).orElse(null)); + if (artifact != null && artifact.getFile() != null) { + return hoverForProject(request, + plugin.getProjectCache().getSnapshotProject(artifact.getFile()).orElse(null), + ParticipantUtils.isWellDefinedDependency(artifactToSearch)); } } + if (hover == null) { - hover = collectArtifactDescription(request, isPlugin); + hover = collectArtifactDescription(request); } return hover; case GOAL_ELT: @@ -136,8 +153,160 @@ public Hover onText(IHoverRequest request) throws Exception { return null; } + + private static final String PomTextHover_managed_version = "The managed version is {0}."; + private static final String PomTextHover_managed_version_missing = "The managed version could not be determined."; + private static final String PomTextHover_managed_location = "The artifact is managed in {0}"; + private static final String PomTextHover_managed_location_missing = "The managed definition location could not be determined, probably defined by \"import\" scoped dependencies."; + + private static String getActualVersionText(boolean supportsMarkdown, MavenProject project) { + if (project == null) { + return null; + } + + String sourceModelId = project.getGroupId() + ':' + project.getArtifactId() + ':' + project.getVersion(); + File locacion = project.getFile(); + return createVersionMessage(supportsMarkdown, project.getVersion(), (locacion != null && locacion.exists() ? sourceModelId : null), locacion.toURI().toString()); + } + + private static String getActualVersionText(boolean supportsMarkdown, Model model) { + if (model == null) { + return null; + } + + Parent parent = model.getParent(); + String version = model.getVersion(); + if (version == null && parent != null) { + version = parent.getVersion(); + } + + InputLocation location = model.getLocation(ARTIFACT_ID_ELT); + if (location == null && parent != null) { + location = parent.getLocation(ARTIFACT_ID_ELT); + } + + return createVersionMessage(supportsMarkdown, version, location); + } - private Hover hoverForProject(boolean supportsMarkdown, MavenProject p) { + private String getManagedVersionText(IHoverRequest request) { + if (!MavenLemminxExtension.match(request.getXMLDocument())) { + return null; + } + + // make sure model is resolved and up-to-date + File currentFolder = new File(URI.create(request.getXMLDocument().getTextDocument().getUri())).getParentFile(); + DOMElement element = ParticipantUtils.findInterestingElement(request.getNode()); + if (element == null || (!DEPENDENCY_ELT.equals(element.getLocalName()) && !PLUGIN_ELT.equals(element.getLocalName()))) { + return null; + } + + boolean isPlugin = PLUGIN_ELT.equals(element.getLocalName()); + MavenProject p = plugin.getProjectCache().getLastSuccessfulMavenProject(element.getOwnerDocument()); + Dependency dependency = ParticipantUtils.getArtifactToSearch(p, request); + + // Search for DEPENDENCY/PLUGIN through the parents pom's + File parentPomFile = getParentPomFile(p, currentFolder, request.getXMLDocument()); + + while (parentPomFile != null && parentPomFile.exists()) { + DOMDocument parentXmlDocument = org.eclipse.lemminx.utils.DOMUtils.loadDocument( + parentPomFile.toURI().toString(), + request.getNode().getOwnerDocument().getResolverExtensionManager()); + if (parentXmlDocument == null) { + return null; // An error occurred while loading the document + } + + MavenProject parentMavenProject = plugin.getProjectCache().getLastSuccessfulMavenProject(parentXmlDocument); + + List elements = findDependencyOrPluginElement(parentXmlDocument, isPlugin, dependency.getGroupId(), dependency.getArtifactId()); + for (DOMElement e : elements) { + Optional version = DOMUtils.findChildElementText(e, VERSION_ELT); + if (version.isPresent()) { + String sourceModelId = parentMavenProject.getGroupId() + ':' + parentMavenProject.getArtifactId() + ':' + parentMavenProject.getVersion(); + return createVersionMessage(request.canSupportMarkupKind(MarkupKind.MARKDOWN), version.get(), sourceModelId, parentPomFile.toURI().toString()); + } + } + + // Else proceed with the next parent + parentPomFile = getParentPomFile(parentMavenProject, currentFolder, parentXmlDocument); + } + return null; + } + + List findDependencyOrPluginElement(DOMDocument document, boolean isPlugin, String groupId, String artifactId) { + return DOMUtils.findNodesByLocalName(document, isPlugin ? PLUGIN_ELT : DEPENDENCY_ELT) + .stream().filter(d -> { + if (!DOMUtils.isADescendantOf(d, isPlugin ? PLUGINS_ELT : DEPENDENCIES_ELT)) { + return false; + } + Optional g = DOMUtils.findChildElementText(d, GROUP_ID_ELT); + Optional a = DOMUtils.findChildElementText(d, ARTIFACT_ID_ELT); + return (g.isPresent() && g.get().equals(groupId) && a.isPresent() && a.get().equals(artifactId)); + }).filter(DOMElement.class::isInstance).map(DOMElement.class::cast).collect(Collectors.toList()); + } + + private File getParentPomFile (MavenProject project, File currentFolder, DOMDocument document) { + Optional projectElement = DOMUtils.findChildElement(document, PROJECT_ELT); + Optional parentElement = projectElement.isPresent() ? DOMUtils.findChildElement(projectElement.get(), PARENT_ELT) : Optional.empty(); + Optional relativePath = parentElement.isPresent() ? DOMUtils.findChildElementText(parentElement.get(), RELATIVE_PATH_ELT) : Optional.empty(); + if (relativePath.isPresent()) { + File relativeFile = new File(currentFolder, relativePath.get()); + if (relativeFile.isDirectory()) { + relativeFile = new File(relativeFile, Maven.POMv4); + } + if (relativeFile.isFile()) { + return relativeFile; + } + } else { + File relativeFile = new File(currentFolder.getParentFile(), Maven.POMv4); + if (relativeFile.isFile()) { + return relativeFile; + } else { + // those next lines may actually be more generic and suit parent definition in any case + if (project != null && project.getParentFile() != null) { + return project.getParentFile(); + } + } + } + return null; + } + + + private static String createVersionMessage(boolean supportsMarkdown, String version, InputLocation location) { + String sourceModelId = null; + String uri = null; + if (location != null) { + InputSource source = location.getSource(); + if (source != null) { + sourceModelId = source.getModelId(); + uri = source.getLocation(); + } + } + + return createVersionMessage(supportsMarkdown, version, sourceModelId, uri); + } + + private static String createVersionMessage(boolean supportsMarkdown, String version, String sourceModelId, String uri) { + UnaryOperator toBold = supportsMarkdown ? MarkdownUtils::toBold : UnaryOperator.identity(); + + String message = null; + if (version != null) { + message = toBold.apply(MessageFormat.format(PomTextHover_managed_version, version)); + } else { + message = toBold.apply(PomTextHover_managed_version_missing); + } + + if (sourceModelId != null) { + message += ' ' + toBold.apply(MessageFormat.format(PomTextHover_managed_location, + supportsMarkdown ? MarkdownUtils.toLink(uri, sourceModelId, null) : sourceModelId)); + } else { + message += ' ' + toBold.apply(PomTextHover_managed_location_missing); + } + + return message; + } + + private Hover hoverForProject(IHoverRequest request, MavenProject p, boolean isWellDefined) { + boolean supportsMarkdown = request.canSupportMarkupKind(MarkupKind.MARKDOWN); UnaryOperator toBold = supportsMarkdown ? MarkdownUtils::toBold : UnaryOperator.identity(); String lineBreak = MarkdownUtils.getLineBreak(supportsMarkdown); @@ -150,6 +319,17 @@ private Hover hoverForProject(boolean supportsMarkdown, MavenProject p) { if (p.getDescription() != null) { message += lineBreak + p.getDescription(); } + + if (!isWellDefined) { + String managedVersion = getManagedVersionText(request); + if (managedVersion == null) { + managedVersion = getActualVersionText(supportsMarkdown, p); + } + if (managedVersion != null) { + message += lineBreak + managedVersion; + } + } + if (message.isBlank()) { return null; } @@ -157,11 +337,12 @@ private Hover hoverForProject(boolean supportsMarkdown, MavenProject p) { message)); } - private Hover collectArtifactDescription(IHoverRequest request, boolean isPlugin) { + private Hover collectArtifactDescription(IHoverRequest request) { boolean supportsMarkdown = request.canSupportMarkupKind(MarkupKind.MARKDOWN); MavenProject p = plugin.getProjectCache().getLastSuccessfulMavenProject(request.getXMLDocument()); Dependency artifactToSearch = ParticipantUtils.getArtifactToSearch(p, request); + boolean wellDefined = ParticipantUtils.isWellDefinedDependency(artifactToSearch); try { ModelBuilder builder = plugin.getProjectCache().getPlexusContainer().lookup(ModelBuilder.class); Optional localDescription = plugin.getLocalRepositorySearcher() @@ -173,8 +354,8 @@ private Hover collectArtifactDescription(IHoverRequest request, boolean isPlugin && (artifactToSearch.getVersion() == null || artifactToSearch.getVersion().equals(gav.getVersion()))) .sorted(Comparator.comparing((Artifact artifact) -> new DefaultArtifactVersion(artifact.getVersion())).reversed()) - .findFirst().map(plugin.getLocalRepositorySearcher()::findLocalFile).map(file -> builder - .buildRawModel(file, ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL, false).get()) + .findFirst().map(plugin.getLocalRepositorySearcher()::findLocalFile).map(file -> + builder.buildRawModel(file, ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL, true).get()) .map(model -> { UnaryOperator toBold = supportsMarkdown ? MarkdownUtils::toBold : UnaryOperator.identity(); @@ -189,6 +370,16 @@ private Hover collectArtifactDescription(IHoverRequest request, boolean isPlugin message += lineBreak + model.getDescription(); } + if (!wellDefined) { + String managedVersion = getManagedVersionText(request); + if (managedVersion == null) { + managedVersion = getActualVersionText(supportsMarkdown, model); + } + if (managedVersion != null) { + message += lineBreak + managedVersion; + } + } + return message; }).map(message -> (message.length() > 2 ? message : null)); if (localDescription.isPresent()) { diff --git a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/utils/MarkdownUtils.java b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/utils/MarkdownUtils.java index 10982e65..7d2af0c5 100644 --- a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/utils/MarkdownUtils.java +++ b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/utils/MarkdownUtils.java @@ -8,6 +8,8 @@ *******************************************************************************/ package org.eclipse.lemminx.extensions.maven.utils; +import java.io.File; + public class MarkdownUtils { public static final String LINE_BREAK = "\n\n"; @@ -43,4 +45,19 @@ public static String htmlXMLToMarkdown(String description) { } return description; } + + public static String toLink(String uri, String message, String title) { + StringBuilder link = new StringBuilder(); + + // [Message](http://example.com/ "Title") + link.append('[').append(message != null ? message : "This link").append(']'); + if (uri != null) { + link.append('(').append(uri); + if (title != null && title.trim().length() > 0) { + link.append(' ').append('"').append(title.trim()).append('"'); + } + link.append(')'); + } + return link.toString(); + } } diff --git a/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/participants/hover/ManagedVersionHoverTest.java b/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/participants/hover/ManagedVersionHoverTest.java new file mode 100644 index 00000000..32731cb0 --- /dev/null +++ b/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/participants/hover/ManagedVersionHoverTest.java @@ -0,0 +1,116 @@ +/******************************************************************************* + * Copyright (c) 2022 Red Hat Inc. and others. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.lemminx.extensions.maven.participants.hover; + +import static org.eclipse.lemminx.extensions.maven.utils.MavenLemminxTestsUtils.createDOMDocument; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutionException; + +import org.eclipse.lemminx.dom.DOMDocument; +import org.eclipse.lemminx.extensions.maven.NoMavenCentralExtension; +import org.eclipse.lemminx.services.XMLLanguageService; +import org.eclipse.lemminx.settings.SharedSettings; +import org.eclipse.lemminx.settings.XMLHoverSettings; +import org.eclipse.lsp4j.Hover; +import org.eclipse.lsp4j.HoverCapabilities; +import org.eclipse.lsp4j.MarkupContent; +import org.eclipse.lsp4j.MarkupKind; +import org.eclipse.lsp4j.Position; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(NoMavenCentralExtension.class) +public class ManagedVersionHoverTest { + + private XMLLanguageService languageService; + + @BeforeEach + public void setUp() throws IOException { + languageService = new XMLLanguageService(); + } + + @AfterEach + public void tearDown() throws InterruptedException, ExecutionException { + languageService.dispose(); + languageService = null; + } + + @Test + @Timeout(90000) + public void testManagedVersionHoverInDependencyChild() throws IOException, URISyntaxException { + DOMDocument document = createDOMDocument("/pom-dependencyManagement-child.xml", languageService); + Hover hover = languageService.doHover(document, new Position(15, 25), createSharedSettings()); + String value = ((MarkupContent) hover.getContents().getRight()).getValue(); + System.out.println("testManagedVersionHoverInDependencyChild: [" + value + "]"); + assertNotNull(value); + assertTrue(value.contains("The managed version is")); + assertTrue(value.contains("2.22.2")); + assertTrue(value.contains("The artifact is managed in")); + assertTrue(value.contains("dependencyManagement:parent:0.0.1-SNAPSHOT")); + assertTrue(value.contains("pom-dependencyManagement-parent.xml")); + } + + @Test + @Timeout(90000) + public void testManagedVersionHoverInDependencyParent() throws IOException, URISyntaxException { + DOMDocument document = createDOMDocument("/pom-dependencyManagement-parent.xml", languageService); + Hover hover = languageService.doHover(document, new Position(13, 29), createSharedSettings()); + String value = ((MarkupContent) hover.getContents().getRight()).getValue(); + System.out.println("testManagedVersionHoverInDependencyParent: [" + value + "]"); + assertNotNull(value); + assertFalse(value.contains("The managed version is")); + assertFalse(value.contains("The artifact is managed in")); + } + + @Test + @Timeout(90000) + public void testManagedVersionHoverInPluginChild() throws IOException, URISyntaxException { + DOMDocument document = createDOMDocument("/pom-pluginManagement-child.xml", languageService); + Hover hover = languageService.doHover(document, new Position(18, 33), createSharedSettings()); + String value = ((MarkupContent) hover.getContents().getRight()).getValue(); + System.out.println("testManagedVersionHoverInPluginChild: [" + value + "]"); + assertNotNull(value); + assertTrue(value.contains("The managed version is")); + assertTrue(value.contains("2.22.2")); + assertTrue(value.contains("The artifact is managed in")); + assertTrue(value.contains("pluginManagement:parent:0.0.1-SNAPSHOT")); + assertTrue(value.contains("pom-pluginManagement-parent.xml")); + } + + @Test + @Timeout(90000) + public void testManagedVersionHoverInPluginParent() throws IOException, URISyntaxException { + DOMDocument document = createDOMDocument("/pom-pluginManagement-parent.xml", languageService); + Hover hover = languageService.doHover(document, new Position(14, 33), createSharedSettings()); + String value = ((MarkupContent) hover.getContents().getRight()).getValue(); + System.out.println("testManagedVersionHoverInPluginParent: [" + value + "]"); + assertNotNull(value); + assertFalse(value.contains("The managed version is")); + assertFalse(value.contains("The artifact is managed in")); + } + + // Enable MARKDOWN format + private static SharedSettings createSharedSettings() { + HoverCapabilities hoverCapabilities = new HoverCapabilities(); + hoverCapabilities.setContentFormat(Arrays.asList(MarkupKind.MARKDOWN)); + SharedSettings sharedSettings = new SharedSettings(); + sharedSettings.getHoverSettings().setCapabilities(hoverCapabilities); + return sharedSettings; + } +}