diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 7162e8658a..0d6221eb8b 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -5,7 +5,7 @@ This file documents all notable changes to https://github.com/devonfw/IDEasy[IDE == 2026.09.002 Release with new features and bugfixes: - +* https://github.com/devonfw/IDEasy/issues/992[#992]: Support multiple attributes for merge:id * https://github.com/devonfw/IDEasy/issues/2190[#2190]: IDEasy destroys my python installation * https://github.com/devonfw/IDEasy/issues/2178[#2178]: Make ReleaseCommandlet independent of specific build commandlet and fix `ide build` using npm instead of yarn * https://github.com/devonfw/IDEasy/issues/2142[#2142]: Move IDE-specific metadata (.idea, .vscode) out of workspace diff --git a/cli/src/main/java/com/devonfw/tools/ide/merge/xml/matcher/IdComputer.java b/cli/src/main/java/com/devonfw/tools/ide/merge/xml/matcher/IdComputer.java index fea8c45abc..fd476a1dbe 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/merge/xml/matcher/IdComputer.java +++ b/cli/src/main/java/com/devonfw/tools/ide/merge/xml/matcher/IdComputer.java @@ -1,6 +1,9 @@ package com.devonfw.tools.ide.merge.xml.matcher; import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; + import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpression; @@ -10,6 +13,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; import org.w3c.dom.NodeList; import com.devonfw.tools.ide.context.IdeContext; @@ -96,7 +100,7 @@ public Element evaluateExpression(Element templateElement, Element workspaceElem * @param element the {@link Element} for which to build the XPath expression * @return the XPath expression as a {@link String}. */ - private String buildXPathExpression(Element element) { + protected String buildXPathExpression(Element element) { String namespaceURI = element.getNamespaceURI(); String localName = element.getLocalName(); @@ -110,7 +114,31 @@ private String buildXPathExpression(Element element) { xpathBuilder.append(prefix).append(":"); } xpathBuilder.append(localName); - if (this.id.startsWith("@")) { + if (this.id.startsWith("@@")) { + xpathBuilder.append('['); + NamedNodeMap attributesMap = element.getAttributes(); + boolean first = true; + for (int i = 0; i < attributesMap.getLength(); i++) { + String attribute = attributesMap.item(i).getNodeName(); + String attributeValue = attributesMap.item(i).getNodeValue(); + if (!attribute.equals("id")) { + appendAttributePredicate(xpathBuilder, attribute, attributeValue, !first); + first = false; + } + } + xpathBuilder.append(']'); + } else if (this.id.startsWith("@") && this.id.contains(",")) { + List attributeNames = Arrays.stream(this.id.split(",")) + .map(attribute -> attribute.substring(1)) + .toList(); + xpathBuilder.append('['); + for (int i = 0; i < attributeNames.size(); i++) { + String attribute = attributeNames.get(i); + String attributeValue = element.getAttribute(attribute); + appendAttributePredicate(xpathBuilder, attribute, attributeValue, i > 0); + } + xpathBuilder.append(']'); + } else if (this.id.startsWith("@")) { String attributeName = this.id.substring(1); String attributeValue = element.getAttribute(attributeName); xpathBuilder.append('[').append(this.id).append("='").append(XmlMergeSupport.escapeSingleQuotes(attributeValue)).append("']"); @@ -128,4 +156,15 @@ private String buildXPathExpression(Element element) { return xpathBuilder.toString(); } + private void appendAttributePredicate(StringBuilder xpathBuilder, String attributeName, String attributeValue, boolean appendAnd) { + if (appendAnd) { + xpathBuilder.append(" and "); + } + xpathBuilder.append('@') + .append(attributeName) + .append("='") + .append(XmlMergeSupport.escapeSingleQuotes(attributeValue)) + .append("'"); + } + } diff --git a/cli/src/test/java/com/devonfw/tools/ide/merge/xml/matcher/IdComputerTest.java b/cli/src/test/java/com/devonfw/tools/ide/merge/xml/matcher/IdComputerTest.java new file mode 100644 index 0000000000..d4d6b734d4 --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/merge/xml/matcher/IdComputerTest.java @@ -0,0 +1,63 @@ +package com.devonfw.tools.ide.merge.xml.matcher; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import java.io.ByteArrayInputStream; +import java.util.stream.Stream; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Test of {@link IdComputer}. + */ +class IdComputerTest { + + @ParameterizedTest + @MethodSource("buildXPathExpressionTestCases") + void testBuildXPathExpression(String elementXml, String mergeId, String expectedXPath) throws Exception { + + // arrange + Element element = parse(elementXml); + IdComputer computer = new IdComputer(mergeId, null); + // act + String actual = computer.buildXPathExpression(element); + // assert + assertThat(actual).isEqualTo(expectedXPath); + } + + private static Stream buildXPathExpressionTestCases() { + + return Stream.of( + arguments("", "@name", "component[@name='RunManager']"), + arguments("", "@default,@type", + "configuration[@default='true' and @type='JUnit']"), + arguments("", "@@", + "configuration[@name='foo' and @type='JUnit']"), + arguments("", "@@", + "configuration[@default='true' and @name='foo' and @type='JUnit']"), + arguments("", "name()", "component[local-name()='component']"), + arguments("myValue", "text()", "value[text()='myValue']")); + } + + /** + * Parses the given XML fragment and returns its root {@link Element}. + * + * @param xml the XML fragment to parse. + * @return the root element of the parsed document + * @throws Exception in case of a parse error + */ + private static Element parse(String xml) throws Exception { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.parse(new ByteArrayInputStream(xml.getBytes())); + return document.getDocumentElement(); + } +}