diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/AddOrUpdateChildTag.java b/rewrite-xml/src/main/java/org/openrewrite/xml/AddOrUpdateChildTag.java
new file mode 100644
index 00000000000..5b44051649c
--- /dev/null
+++ b/rewrite-xml/src/main/java/org/openrewrite/xml/AddOrUpdateChildTag.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openrewrite.xml;
+
+import lombok.EqualsAndHashCode;
+import lombok.Value;
+import org.intellij.lang.annotations.Language;
+import org.jspecify.annotations.Nullable;
+import org.openrewrite.*;
+import org.openrewrite.xml.tree.Xml;
+
+@Value
+@EqualsAndHashCode(callSuper = false)
+public class AddOrUpdateChildTag extends Recipe {
+
+ @Option(displayName = "Parent XPath",
+ description = "XPath identifying the parent to which a child tag must be added",
+ example = "/project//plugin//configuration")
+ @Language("xpath")
+ String parentXPath;
+
+ @Option(displayName = "New child tag",
+ description = "The XML of the new child to add or update on the parent tag.",
+ example = "true ")
+ @Language("xml")
+ String newChildTag;
+
+ @Option(displayName = "Replace existing child",
+ description = "Set to `false` to not replace the child tag if it already exists. Defaults to true.",
+ required = false)
+ @Nullable
+ Boolean replaceExisting;
+
+ @Override
+ public String getDisplayName() {
+ return "Add or update child tag";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Adds or updates a child element below the parent(s) matching the provided `parentXPath` expression. " +
+ "If a child with the same name already exists, it will be replaced by default. Otherwise, a new child will be added. " +
+ "This ensures idempotent behaviour.";
+ }
+
+ @Override
+ public Validated validate() {
+ Validated validated = super.validate()
+ .and(Validated.notBlank("parentXPath", parentXPath))
+ .and(Validated.notBlank("newChildTag", newChildTag));
+ try {
+ Xml.Tag.build(newChildTag);
+ } catch (Exception e) {
+ validated = validated.and(Validated.invalid("newChildTag", newChildTag, "Invalid XML for child tag", e));
+ }
+ return validated;
+ }
+
+ @Override
+ public TreeVisitor, ExecutionContext> getVisitor() {
+ return new XmlVisitor() {
+ private final XPathMatcher xPathMatcher = new XPathMatcher(parentXPath);
+
+ @Override
+ public Xml visitTag(Xml.Tag tag, ExecutionContext ctx) {
+ if (xPathMatcher.matches(getCursor())) {
+ Xml.Tag newChild = Xml.Tag.build(newChildTag);
+ if (replaceExisting == null || replaceExisting || !tag.getChild(newChild.getName()).isPresent()) {
+ return AddOrUpdateChild.addOrUpdateChild(tag, newChild, getCursor().getParentOrThrow());
+ }
+ }
+ return super.visitTag(tag, ctx);
+ }
+ };
+ }
+}
diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/XPathMatcher.java b/rewrite-xml/src/main/java/org/openrewrite/xml/XPathMatcher.java
index b3c184b9038..c3fbd8fe165 100644
--- a/rewrite-xml/src/main/java/org/openrewrite/xml/XPathMatcher.java
+++ b/rewrite-xml/src/main/java/org/openrewrite/xml/XPathMatcher.java
@@ -48,12 +48,14 @@ public class XPathMatcher {
private final boolean startsWithSlash;
private final boolean startsWithDoubleSlash;
private final String[] parts;
+ private final long tagMatchingParts;
public XPathMatcher(String expression) {
this.expression = expression;
startsWithSlash = expression.startsWith("/");
startsWithDoubleSlash = expression.startsWith("//");
parts = splitOnXPathSeparator(expression.substring(startsWithDoubleSlash ? 2 : startsWithSlash ? 1 : 0));
+ tagMatchingParts = Arrays.stream(parts).filter(part -> !part.isEmpty() && !part.startsWith("@")).count();
}
private String[] splitOnXPathSeparator(String input) {
@@ -92,18 +94,8 @@ public boolean matches(Cursor cursor) {
if (index < 0) {
return false;
}
- if (part.startsWith("@")) { // is attribute selector
- partWithCondition = part;
- tagForCondition = i > 0 ? path.get(i - 1) : path.get(i);
- } else { // is element selector
- if (part.charAt(index + 1) == '@') { // is Attribute condition
- partWithCondition = part;
- tagForCondition = path.get(i);
- } else if (part.contains("(") && part.contains(")")) { // is function condition
- partWithCondition = part;
- tagForCondition = path.get(i);
- }
- }
+ partWithCondition = part;
+ tagForCondition = path.get(pathIndex);
} else if (i < path.size() && i > 0 && parts[i - 1].endsWith("]")) {
String partBefore = parts[i - 1];
int index = partBefore.indexOf("[");
@@ -117,6 +109,7 @@ public boolean matches(Cursor cursor) {
}
} else if (part.endsWith(")")) { // is xpath method
// TODO: implement other xpath methods
+ throw new UnsupportedOperationException("XPath methods are not supported");
}
String partName;
@@ -164,36 +157,42 @@ public boolean matches(Cursor cursor) {
}
}
- return startsWithSlash || path.size() - pathIndex <= 1;
+ // we have matched the whole XPath, and it does not start with the root
+ return true;
} else {
Collections.reverse(path);
// Deal with the two forward slashes in the expression; works, but I'm not proud of it.
- if (expression.contains("//") && !expression.contains("://") && Arrays.stream(parts).anyMatch(StringUtils::isBlank)) {
+ if (expression.contains("//") && Arrays.stream(parts).anyMatch(StringUtils::isBlank)) {
int blankPartIndex = Arrays.asList(parts).indexOf("");
int doubleSlashIndex = expression.indexOf("//");
- if (path.size() > blankPartIndex && path.size() >= parts.length - 1) {
- String newExpression;
- if (Objects.equals(path.get(blankPartIndex).getName(), parts[blankPartIndex + 1])) {
- newExpression = String.format(
- "%s/%s",
- expression.substring(0, doubleSlashIndex),
- expression.substring(doubleSlashIndex + 2)
- );
- } else {
- newExpression = String.format(
- "%s/%s/%s",
- expression.substring(0, doubleSlashIndex),
- path.get(blankPartIndex).getName(),
- expression.substring(doubleSlashIndex + 2)
- );
+ if (path.size() > blankPartIndex && path.size() >= tagMatchingParts) {
+ Xml.Tag blankPartTag = path.get(blankPartIndex);
+ String part = parts[blankPartIndex + 1];
+ Matcher matcher = ELEMENT_WITH_CONDITION_PATTERN.matcher(part);
+ if (matcher.matches() ?
+ matchesElementWithConditionFunction(matcher, blankPartTag, cursor) != null :
+ Objects.equals(blankPartTag.getName(), part)) {
+ if (matchesWithoutDoubleSlashesAt(cursor, doubleSlashIndex)) {
+ return true;
+ }
+ // fall-through: maybe we can skip this element and match further down
}
+ String newExpression = String.format(
+ // the // here allows to skip several levels of nested elements
+ "%s/%s//%s",
+ expression.substring(0, doubleSlashIndex),
+ blankPartTag.getName(),
+ expression.substring(doubleSlashIndex + 2)
+ );
return new XPathMatcher(newExpression).matches(cursor);
+ } else if (path.size() == tagMatchingParts) {
+ return matchesWithoutDoubleSlashesAt(cursor, doubleSlashIndex);
}
}
- if (parts.length > path.size() + 1) {
+ if (tagMatchingParts > path.size()) {
return false;
}
@@ -235,6 +234,24 @@ public boolean matches(Cursor cursor) {
}
}
+ private boolean matchesWithoutDoubleSlashesAt(Cursor cursor, int doubleSlashIndex) {
+ String newExpression = String.format(
+ "%s/%s",
+ expression.substring(0, doubleSlashIndex),
+ expression.substring(doubleSlashIndex + 2)
+ );
+ return new XPathMatcher(newExpression).matches(cursor);
+ }
+
+ /**
+ * Checks that the given {@code tag} matches the XPath part represented by {@code matcher}.
+ *
+ * @param matcher an XPath part matcher for {@link #ELEMENT_WITH_CONDITION_PATTERN}
+ * @param tag a tag to match
+ * @param cursor the cursor we are trying to match
+ * @return the element name specified before the condition of the part
+ * (either {@code tag.getName()}, {@code "*"} or an attribute name) or {@code null} if the tag did not match
+ */
private @Nullable String matchesElementWithConditionFunction(Matcher matcher, Xml.Tag tag, Cursor cursor) {
boolean isAttributeElement = matcher.group(1) != null;
String element = matcher.group(2);
diff --git a/rewrite-xml/src/test/java/org/openrewrite/xml/AddOrUpdateChildTagTest.java b/rewrite-xml/src/test/java/org/openrewrite/xml/AddOrUpdateChildTagTest.java
new file mode 100644
index 00000000000..49bdce28b09
--- /dev/null
+++ b/rewrite-xml/src/test/java/org/openrewrite/xml/AddOrUpdateChildTagTest.java
@@ -0,0 +1,496 @@
+/*
+ * Copyright 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openrewrite.xml;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.NullSource;
+import org.openrewrite.test.RewriteTest;
+
+import static org.openrewrite.xml.Assertions.xml;
+
+class AddOrUpdateChildTagTest implements RewriteTest {
+ @ParameterizedTest
+ @NullSource
+ @CsvSource({"true", "false"})
+ void addsTagEverywhereWhenAbsent(Boolean replaceExisting) {
+ rewriteRun(spec -> spec.recipe(new AddOrUpdateChildTag(
+ "/project//plugin[groupId='org.apache.maven.plugins' and artifactId='maven-resources-plugin']" +
+ "//configuration",
+ "true ",
+ replaceExisting)),
+ xml(
+ """
+
+
+ 4.0.0
+ com.example
+ my-project
+ 1.0
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.3.1
+
+ UTF-8
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.3.1
+
+
+
+ UTF-8
+
+
+
+
+
+
+
+
+ profile1
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.3.1
+
+
+
+ UTF-8
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.3.1
+
+ UTF-8
+
+
+
+
+
+
+
+ """,
+ """
+
+
+ 4.0.0
+ com.example
+ my-project
+ 1.0
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.3.1
+
+ UTF-8
+ true
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.3.1
+
+
+
+ UTF-8
+ true
+
+
+
+
+
+
+
+
+ profile1
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.3.1
+
+
+
+ UTF-8
+ true
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.3.1
+
+ UTF-8
+ true
+
+
+
+
+
+
+
+ """
+ )
+ );
+ }
+
+ @ParameterizedTest
+ @NullSource
+ @CsvSource("true")
+ void updateTagEverywhere(Boolean replaceExisting) {
+ rewriteRun(spec -> spec.recipe(new AddOrUpdateChildTag(
+ "/project//plugin[groupId='org.apache.maven.plugins' and artifactId='maven-resources-plugin']" +
+ "//configuration",
+ "true ",
+ replaceExisting)),
+ xml(
+ """
+
+
+ 4.0.0
+ com.example
+ my-project
+ 1.0
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.3.1
+
+ UTF-8
+ false
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.3.1
+
+ UTF-8
+ false
+
+
+
+
+
+
+ profile1
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.3.1
+
+ UTF-8
+ false
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.3.1
+
+ UTF-8
+ false
+
+
+
+
+
+
+
+ """,
+ """
+
+
+ 4.0.0
+ com.example
+ my-project
+ 1.0
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.3.1
+
+ UTF-8
+ true
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.3.1
+
+ UTF-8
+ true
+
+
+
+
+
+
+ profile1
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.3.1
+
+ UTF-8
+ true
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.3.1
+
+ UTF-8
+ true
+
+
+
+
+
+
+
+ """
+ )
+ );
+ }
+
+ @Test
+ void dontUpdateIfReplaceIsDisabled() {
+ rewriteRun(spec -> spec.recipe(new AddOrUpdateChildTag(
+ "/project//plugin[groupId='org.apache.maven.plugins' and artifactId='maven-resources-plugin']" +
+ "//configuration",
+ "true ",
+ false)),
+ xml(
+ """
+
+
+ 4.0.0
+ com.example
+ my-project
+ 1.0
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.3.1
+
+ UTF-8
+ false
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.3.1
+
+ UTF-8
+ false
+
+
+
+
+
+
+ profile1
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.3.1
+
+ UTF-8
+ false
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.3.1
+
+ UTF-8
+ false
+
+
+
+
+
+
+
+ """
+ )
+ );
+ }
+
+ @ParameterizedTest
+ @NullSource
+ @CsvSource({"true", "false"})
+ void dontTouchAnythingElse(Boolean replaceExisting) {
+ rewriteRun(spec -> spec.recipe(new AddOrUpdateChildTag(
+ "/project//plugin[groupId='org.apache.maven.plugins' and artifactId='maven-resources-plugin']" +
+ "//configuration",
+ "true ",
+ replaceExisting)),
+ xml(
+ """
+
+
+ 4.0.0
+ com.example
+ my-project
+ 1.0
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-other-plugin
+ 3.3.1
+
+ UTF-8
+ false
+
+
+
+
+
+
+ org.apache.other.plugins
+ maven-resources-plugin
+ 3.3.1
+
+ UTF-8
+ false
+
+
+
+
+
+
+ profile1
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.3.1
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-other-plugin
+ 3.3.1
+
+ UTF-8
+ false
+
+
+
+
+
+
+
+ """
+ )
+ );
+ }
+}
diff --git a/rewrite-xml/src/test/java/org/openrewrite/xml/XPathMatcherTest.java b/rewrite-xml/src/test/java/org/openrewrite/xml/XPathMatcherTest.java
index f369bfda2c2..31b5363800b 100755
--- a/rewrite-xml/src/test/java/org/openrewrite/xml/XPathMatcherTest.java
+++ b/rewrite-xml/src/test/java/org/openrewrite/xml/XPathMatcherTest.java
@@ -118,7 +118,13 @@ void matchAbsolute() {
assertThat(match("/dependencies/dependency", xmlDoc)).isTrue();
assertThat(match("/dependencies/*/artifactId", xmlDoc)).isTrue();
assertThat(match("/dependencies/*", xmlDoc)).isTrue();
+ assertThat(match("/dependencies//dependency", xmlDoc)).isTrue();
+ assertThat(match("/dependencies//dependency//groupId", xmlDoc)).isTrue();
+
+ // negative matches
assertThat(match("/dependencies/dne", xmlDoc)).isFalse();
+ assertThat(match("/dependencies//dne", xmlDoc)).isFalse();
+ assertThat(match("/dependencies//dependency//dne", xmlDoc)).isFalse();
}
@Test
@@ -127,6 +133,17 @@ void matchAbsoluteAttribute() {
assertThat(match("/dependencies/dependency/artifactId/@scope", xmlDoc)).isTrue();
assertThat(match("/dependencies/dependency/artifactId/@*", xmlDoc)).isTrue();
assertThat(match("/dependencies/dependency/groupId/@*", xmlDoc)).isFalse();
+ assertThat(match("/dependencies//dependency//@scope", xmlDoc)).isTrue();
+ assertThat(match("/dependencies//dependency//artifactId//@scope", xmlDoc)).isTrue();
+ assertThat(match("/dependencies//dependency//@*", xmlDoc)).isTrue();
+ assertThat(match("/dependencies//dependency//artifactId//@*", xmlDoc)).isTrue();
+
+ // negative matches
+ assertThat(match("/dependencies/dependency/artifactId/@dne", xmlDoc)).isFalse();
+ assertThat(match("/dependencies/dependency/artifactId/@dne", xmlDoc)).isFalse();
+ assertThat(match("/dependencies//dependency//@dne", xmlDoc)).isFalse();
+ assertThat(match("/dependencies//dependency//artifactId//@dne", xmlDoc)).isFalse();
+
}
@Test
@@ -136,8 +153,6 @@ void matchRelative() {
assertThat(match("//dependency", xmlDoc)).isTrue();
assertThat(match("dependency/*", xmlDoc)).isTrue();
assertThat(match("dne", xmlDoc)).isFalse();
- assertThat(match("/dependencies//dependency", xmlDoc)).isTrue();
- assertThat(match("/dependencies//dependency/groupId", xmlDoc)).isTrue();
}
@Test
@@ -147,6 +162,51 @@ void matchRelativeAttribute() {
assertThat(match("//dependency/artifactId/@scope", xmlDoc)).isTrue();
}
+ @Test
+ void matchNestedElementsWithSameName() {
+ var xml = new XmlParser().parse(
+ """
+
+
+
+
+ auie
+
+
+
+
+ """
+ ).toList().get(0);
+
+ // no / at start
+ assertThat(match("element/test", xml)).isTrue();
+ assertThat(match("element[@foo='bar']/test", xml)).isTrue();
+ assertThat(match("element[@foo='baz']/test", xml)).isFalse();
+ assertThat(match("element/@qux", xml)).isTrue();
+ assertThat(match("dne[@foo='bar']/test", xml)).isFalse();
+
+ // // at start
+ assertThat(match("//element/test", xml)).isTrue();
+ assertThat(match("//element[@foo='bar']/test", xml)).isTrue();
+ assertThat(match("//element[@foo='baz']/test", xml)).isFalse();
+ assertThat(match("//element/@qux", xml)).isTrue();
+ assertThat(match("//dne[@foo='bar']/test", xml)).isFalse();
+
+ // TODO // in the middle without / (or with //) at start (not currently supported)
+// assertThat(match("root//element/test", xml)).isTrue();
+// assertThat(match("root//element[@foo='bar']/test", xml)).isTrue();
+// assertThat(match("root//element[@foo='baz']/test", xml)).isFalse();
+// assertThat(match("root//element/@qux", xml)).isTrue();
+// assertThat(match("root//dne[@foo='bar']/test", xml)).isFalse();
+
+ // // in the middle with / at start
+ assertThat(match("/root//element/test", xml)).isTrue();
+ assertThat(match("/root//element[@foo='bar']/test", xml)).isTrue();
+ assertThat(match("/root//element[@foo='baz']/test", xml)).isFalse();
+ assertThat(match("/root//element/@qux", xml)).isTrue();
+ assertThat(match("/root//dne[@foo='bar']/test", xml)).isFalse();
+ }
+
@Test
void matchPom() {
assertThat(match("/project/build/plugins/plugin/configuration/source",
@@ -163,8 +223,10 @@ void matchPom() {
pomXml1)).isTrue();
assertThat(match("/project/build//plugins/plugin/configuration/source",
pomXml2)).isTrue();
-// assertThat(match("/project/build//plugin/configuration/source", pomXml2)).isTrue(); // TODO: seems parser only handles // up to 1 level
-// assertThat(match("/project//configuration/source", pomXml2)).isTrue(); // TODO: was already failing previously
+ // skip 2+ levels with //
+ assertThat(match("/project/build//plugin/configuration/source", pomXml2)).isTrue();
+ assertThat(match("/project//configuration/source", pomXml2)).isTrue();
+ assertThat(match("/project//plugin//source", pomXml2)).isTrue();
}
private final SourceFile attributeXml = new XmlParser().parse(
@@ -203,7 +265,7 @@ void wildcards() {
// relative xpath with wildcard element
assertThat(match("//*[@foo='bar']", attributeXml)).isTrue();
assertThat(match("//*[@foo='baz']", attributeXml)).isFalse();
-// assertThat(match("//*[foo='bar']", attributeXml)).isFalse(); // TODO: fix relative xpath with condition
+ assertThat(match("//*[foo='bar']", attributeXml)).isFalse();
assertThat(match("//*[foo='baz']", attributeXml)).isTrue();
}
@@ -220,7 +282,7 @@ void relativePathsWithConditions() {
"""
).toList().get(0);
-// assertThat(match("//element1[foo='bar']", xml)).isFalse(); // TODO: fix - was already failing before * changes
+ assertThat(match("//element1[foo='bar']", xml)).isFalse();
assertThat(match("//element1[foo='baz']", xml)).isTrue();
assertThat(match("//element1[@foo='bar']", xml)).isTrue();
assertThat(match("//element1[foo='baz']/test", xml)).isTrue();
@@ -275,8 +337,7 @@ void matchLocalNameFunctionCondition() {
assertThat(match("//ns2:element2[local-name()='element2']", namespacedXml)).isTrue();
assertThat(match("//dne[local-name()='dne']", namespacedXml)).isFalse();
- // TODO: fix mid-path // with condition
-// assertThat(match("/root//element1[local-name()='element1']", namespacedXml)).isTrue();
+ assertThat(match("/root//element1[local-name()='element1']", namespacedXml)).isTrue();
}
@Test
@@ -300,9 +361,8 @@ void matchAttributeCondition() {
assertThat(match("//element1[@*='dne']", namespacedXml)).isFalse();
assertThat(match("/root/element1[@*='content3']", namespacedXml)).isTrue();
assertThat(match("/root/element1[@*='dne']", namespacedXml)).isFalse();
- // TODO: fix mid-path // match with condition
-// assertThat(match("/root//element1[@*='content3']", namespacedXml)).isTrue();
-// assertThat(match("/root//element1[@*='dne']", namespacedXml)).isFalse();
+ assertThat(match("/root//element1[@*='content3']", namespacedXml)).isTrue();
+ assertThat(match("/root//element1[@*='dne']", namespacedXml)).isFalse();
}
@Test
@@ -327,9 +387,8 @@ void matchAttributeElement() {
assertThat(match("/root/parent/element3/@ns3:attr[namespace-uri()='http://www.example.com/namespace3']", namespacedXml)).isTrue();
assertThat(match("//element3/@ns3:attr[namespace-uri()='http://www.example.com/namespace3']", namespacedXml)).isTrue();
- // TODO: fix mid-path // match with attribute element
-// assertThat(match("/root//element1/@*", namespacedXml)).isTrue();
-// assertThat(match("/root//element1/@*[namespace-uri()='http://www.example.com/namespace3']", namespacedXml)).isTrue();
+ assertThat(match("/root//element1/@*", namespacedXml)).isTrue();
+ assertThat(match("/root//element1/@*[namespace-uri()='http://www.example.com/namespace3']", namespacedXml)).isTrue();
}
@Test