diff --git a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/DOMConstants.java b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/DOMConstants.java index 00d85b03..704dd778 100644 --- a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/DOMConstants.java +++ b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/DOMConstants.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2021 Red Hat Inc. and others. + * Copyright (c) 2021-2023 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/ @@ -18,6 +18,7 @@ private DOMConstants() { } + public static final String PACKAGING_ELT = "packaging"; public static final String PROJECT_ELT = "project"; public static final String MODULE_ELT = "module"; public static final String RELATIVE_PATH_ELT = "relativePath"; @@ -59,4 +60,12 @@ private DOMConstants() { public static final String FILE_ELT = "file"; public static final String EXISTS_ELT = "exists"; public static final String MISSING_ELT = "missing"; + + // Packaging values + public static final String PACKAGING_TYPE_JAR ="jar"; + public static final String PACKAGING_TYPE_WAR = "war"; + public static final String PACKAGING_TYPE_EJB = "ejb"; + public static final String PACKAGING_TYPE_EAR = "ear"; + public static final String PACKAGING_TYPE_POM = "pom"; + public static final String PACKAGING_TYPE_MAVEN_PLUGIN = "maven-plugin"; } diff --git a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/MavenProjectCache.java b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/MavenProjectCache.java index 1273189c..18868dfe 100644 --- a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/MavenProjectCache.java +++ b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/MavenProjectCache.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2020-2022 Red Hat Inc. and others. + * Copyright (c) 2020-2023 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/ @@ -202,6 +202,10 @@ public InputStream getInputStream() throws IOException { } private ProjectBuildingRequest newProjectBuildingRequest() { + return newProjectBuildingRequest(true); + } + + private ProjectBuildingRequest newProjectBuildingRequest(boolean resolveDependencies) { ProjectBuildingRequest request = new DefaultProjectBuildingRequest(); request.setSystemProperties(mavenRequest.getSystemProperties()); request.setLocalRepository(mavenRequest.getLocalRepository()); @@ -209,7 +213,7 @@ private ProjectBuildingRequest newProjectBuildingRequest() { request.setPluginArtifactRepositories(mavenRequest.getPluginArtifactRepositories()); // TODO more to transfer from mavenRequest to ProjectBuildingRequest? request.setRepositorySession(lemminxMavenPlugin.getMavenSession().getRepositorySession()); - request.setResolveDependencies(true); + request.setResolveDependencies(resolveDependencies); return request; } @@ -234,8 +238,12 @@ public Collection getProjects() { } public MavenProject getSnapshotProject(DOMDocument document, String profileId) { + return getSnapshotProject(document, profileId, true); + } + + public MavenProject getSnapshotProject(DOMDocument document, String profileId, boolean resolveDependencies) { // it would be nice to directly rebuild from Model instead of reparsing text - ProjectBuildingRequest request = newProjectBuildingRequest(); + ProjectBuildingRequest request = newProjectBuildingRequest(resolveDependencies); if (profileId != null) { request.setActiveProfileIds(List.of(profileId)); } diff --git a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/completion/MavenCompletionParticipant.java b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/completion/MavenCompletionParticipant.java index a32ef7ac..993cf1b5 100644 --- a/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/completion/MavenCompletionParticipant.java +++ b/lemminx-maven/src/main/java/org/eclipse/lemminx/extensions/maven/participants/completion/MavenCompletionParticipant.java @@ -22,6 +22,7 @@ import static org.eclipse.lemminx.extensions.maven.DOMConstants.MISSING_ELT; import static org.eclipse.lemminx.extensions.maven.DOMConstants.MODULE_ELT; import static org.eclipse.lemminx.extensions.maven.DOMConstants.OUTPUT_DIRECTORY_ELT; +import static org.eclipse.lemminx.extensions.maven.DOMConstants.PACKAGING_ELT; import static org.eclipse.lemminx.extensions.maven.DOMConstants.PARENT_ELT; import static org.eclipse.lemminx.extensions.maven.DOMConstants.PHASE_ELT; import static org.eclipse.lemminx.extensions.maven.DOMConstants.PLUGINS_ELT; @@ -35,6 +36,13 @@ import static org.eclipse.lemminx.extensions.maven.DOMConstants.TEST_SOURCE_DIRECTORY_ELT; import static org.eclipse.lemminx.extensions.maven.DOMConstants.VERSION_ELT; +import static org.eclipse.lemminx.extensions.maven.DOMConstants.PACKAGING_TYPE_JAR; +import static org.eclipse.lemminx.extensions.maven.DOMConstants.PACKAGING_TYPE_WAR; +import static org.eclipse.lemminx.extensions.maven.DOMConstants.PACKAGING_TYPE_EJB; +import static org.eclipse.lemminx.extensions.maven.DOMConstants.PACKAGING_TYPE_EAR; +import static org.eclipse.lemminx.extensions.maven.DOMConstants.PACKAGING_TYPE_POM; +import static org.eclipse.lemminx.extensions.maven.DOMConstants.PACKAGING_TYPE_MAVEN_PLUGIN; + import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; @@ -45,7 +53,9 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -58,17 +68,24 @@ import java.util.concurrent.TimeoutException; import java.util.function.Function; import java.util.function.Predicate; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.maven.Maven; +import org.apache.maven.artifact.handler.ArtifactHandler; import org.apache.maven.artifact.versioning.DefaultArtifactVersion; import org.apache.maven.model.Dependency; import org.apache.maven.model.Model; +import org.apache.maven.model.Plugin; import org.apache.maven.model.io.xpp3.MavenXpp3Writer; import org.apache.maven.plugin.InvalidPluginDescriptorException; import org.apache.maven.plugin.PluginDescriptorParsingException; @@ -77,6 +94,8 @@ import org.apache.maven.plugin.descriptor.PluginDescriptor; import org.apache.maven.project.MavenProject; import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.artifact.DefaultArtifactType; import org.eclipse.lemminx.commons.BadLocationException; import org.eclipse.lemminx.dom.DOMDocument; import org.eclipse.lemminx.dom.DOMElement; @@ -106,6 +125,10 @@ import org.eclipse.lsp4j.TextEdit; import org.eclipse.lsp4j.jsonrpc.CancelChecker; import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; public class MavenCompletionParticipant extends CompletionParticipantAdapter { @@ -117,7 +140,15 @@ public class MavenCompletionParticipant extends CompletionParticipantAdapter { private static final String FILE_TYPE = "File"; private static final String STRING_TYPE = "File"; private static final String DIRECTORY_STRING_LC = "directory"; - + + // Extension packaging types: components.xml path and element names + private static final String COMPONENTS_PATH = "META-INF/plexus/components.xml"; + private static final String JAR_EXT = ".jar"; + private static final String COMPONENTS_COMPONENT_ELT = "component"; + private static final String COMPONENTS_ROLE_ELT = "role"; + private static final String COMPONENTS_CONFIGURATION_ELT = "configuration"; + private static final String COMPONENTS_TYPE_ELT = "type"; + static interface GAVInsertionStrategy { /** * set current element value and add siblings as addition textEdits @@ -361,6 +392,9 @@ public void onXMLContent(ICompletionRequest request, ICompletionResponse respons case GOAL_ELT: collectGoals(request).forEach(response::addCompletionItem); break; + case PACKAGING_ELT: + collectPackaging(request).forEach(response::addCompletionItem); + break; default: Set parameters = MavenPluginUtils.collectPluginConfigurationMojoParameters(request, plugin) .stream().filter(p -> p.name.equals(parent.getLocalName())) @@ -490,6 +524,95 @@ private Collection collectGoals(ICompletionRequest request) { return Collections.emptySet(); } + private Collection collectPackaging(ICompletionRequest request) { + Set packagingTypes = new LinkedHashSet<>(); + packagingTypes.add(PACKAGING_TYPE_JAR); + packagingTypes.add(PACKAGING_TYPE_WAR); + packagingTypes.add(PACKAGING_TYPE_EAR); + packagingTypes.add(PACKAGING_TYPE_EJB); + packagingTypes.add(PACKAGING_TYPE_POM); + packagingTypes.add(PACKAGING_TYPE_MAVEN_PLUGIN); + + // dynamically load available packaging types from build plugins + updateAvailablePackagingTypes(packagingTypes, request); + + return packagingTypes.stream().map(type -> { + try { + CompletionItem item = toTextCompletionItem(request, type); + item.setDocumentation("Packagng Type: " + (type != null ? type : "unknown")); + item.setKind(CompletionItemKind.Value); + item.setSortText(type != null ? type : "zzz"); + return item; + } catch (BadLocationException e) { + LOGGER.log(Level.SEVERE, e.getMessage(), e); + return toErrorCompletionItem(e); + } + }).collect(Collectors.toList()); + } + + private void updateAvailablePackagingTypes(Set packagingTypes, ICompletionRequest request) { + MavenProject project = plugin.getProjectCache().getSnapshotProject(request.getXMLDocument(), null, false); + if (project == null) { + return; + } + + for (Plugin plugin : project.getBuildPlugins()) { + if (plugin.isExtensions()) { + Artifact artifact = new DefaultArtifact( + plugin.getGroupId(), plugin.getArtifactId(), + null, null, plugin.getVersion(), + new HashMap(), + new DefaultArtifactType("maven-plugin")); + addPluginPackagingTypes(packagingTypes, artifact); + } + } + } + + /** + * Parses the plugin's META-INF/plexus/components.xml file for available + * packaging types + * + * @param packagingTypes Set of packaging types that this method will add to + * @param artifact The artifact of the build plugin + * @apiNote If any exceptions occur during this method, such as an XML parsing + * exception or file not found, this method will immediately stop. It + * is assumed that there is something wrong with the user's project or + * repository setup which prevents this method from completing. + */ + private void addPluginPackagingTypes(Set packagingTypes, Artifact artifact) { + File artifactFile = plugin.getLocalRepositorySearcher().findLocalFile(artifact); + if (artifactFile == null) { + return; + } + + try (JarFile jarFile = new JarFile(artifactFile.getAbsoluteFile() + JAR_EXT)) { + DocumentBuilder db = DocumentBuilderFactory.newDefaultInstance().newDocumentBuilder(); + JarEntry componentsxml = jarFile.getJarEntry(COMPONENTS_PATH); + if (componentsxml != null) { + Document doc = db.parse(jarFile.getInputStream(componentsxml)); + doc.getDocumentElement().normalize(); + NodeList components = doc.getElementsByTagName(COMPONENTS_COMPONENT_ELT); + for (int i = 0; i < components.getLength(); i++) { + Node component = components.item(i); + if (component.getNodeType() == Node.ELEMENT_NODE) { + Element element = (Element) component; + String role = element.getElementsByTagName(COMPONENTS_ROLE_ELT).item(0).getTextContent(); + if (ArtifactHandler.ROLE.equals(role)) { + Node config = element.getElementsByTagName(COMPONENTS_CONFIGURATION_ELT).item(0); + if (config.getNodeType() == Node.ELEMENT_NODE) { + Element configEl = (Element) config; + String name = configEl.getElementsByTagName(COMPONENTS_TYPE_ELT).item(0).getTextContent(); + packagingTypes.add(name); + } + } + } + } + } + } catch (Exception e) { + // Broken XML, file not found, etc. Can't add packaging types. + } + } + private CompletionItem toGAVCompletionItem(ArtifactWithDescription artifactInfo, ICompletionRequest request, GAVInsertionStrategy strategy) { boolean hasGroupIdSet = DOMUtils.findChildElementText(request.getParentElement().getParentElement(), GROUP_ID_ELT).isPresent() diff --git a/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/participants/completion/MavenCompletionParticipantTest.java b/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/participants/completion/MavenCompletionParticipantTest.java index 28a42341..013d309c 100644 --- a/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/participants/completion/MavenCompletionParticipantTest.java +++ b/lemminx-maven/src/test/java/org/eclipse/lemminx/extensions/maven/participants/completion/MavenCompletionParticipantTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2021-2022 Red Hat Inc. and others. + * Copyright (c) 2021-2023 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/ @@ -311,4 +311,29 @@ public void testPluginParameterCompletion() throws Exception { "$0"), "$0", null)); } + + @Test + public void testPackagingyCompletion() throws Exception { + String pom = """ + + + 4.0.0 + pom-with-packaging + org.eclipse.lemminx.extention.maven.tests + 0.1.0 + | + + """; + testCompletionFor(pom, null, "file:///pom.xml", null, // + c("jar", te(10, 13, 10, 13, "jar"), "jar"), + c("war", te(10, 13, 10, 13, "war"), "war"), + c("ear", te(10, 13, 10, 13, "ear"), "ear"), + c("ejb", te(10, 13, 10, 13, "ejb"), "ejb"), + c("pom", te(10, 13, 10, 13, "pom"), "pom"), + c("maven-plugin", te(10, 13, 10, 13, "maven-plugin"), "maven-plugin")); + } } \ No newline at end of file