Skip to content

Commit

Permalink
XPathMatcher does not support paths containing "//" and conditions #4119
Browse files Browse the repository at this point in the history
 (#4120)

* testcases

* my testcases are working but relative paths in general dont

* safety commit

* safety commit

* most examples work

* extracted condition for readability

* delete condition block from part for comparisson of elementname

* changed tagForCondition on the correct element

* reformatted code

* Polish code style

* Polish: remove a comment

* Polish: Remove some Stream API usages

---------

Co-authored-by: Knut Wannheden <[email protected]>
  • Loading branch information
SilasSchaprian and knutwannheden authored Apr 5, 2024
1 parent 489b8eb commit 4159303
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 17 deletions.
101 changes: 84 additions & 17 deletions rewrite-xml/src/main/java/org/openrewrite/xml/XPathMatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import org.openrewrite.Cursor;
import org.openrewrite.internal.StringUtils;
import org.openrewrite.internal.lang.Nullable;
import org.openrewrite.xml.search.FindTags;
import org.openrewrite.xml.tree.Xml;

Expand All @@ -25,15 +26,16 @@
import java.util.regex.Pattern;

/**
* Supports a limited set of XPath expressions, specifically those
* documented on <a href="https://www.w3schools.com/xml/xpath_syntax.asp">this page</a>.
* Supports a limited set of XPath expressions, specifically those documented on <a
* href="https://www.w3schools.com/xml/xpath_syntax.asp">this page</a>.
* <p>
* Used for checking whether a visitor's cursor meets a certain XPath expression.
* <p>
* The "current node" for XPath evaluation is always the root node of the document.
* As a result, '.' and '..' are not recognized.
* The "current node" for XPath evaluation is always the root node of the document. As a result, '.' and '..' are not
* recognized.
*/
public class XPathMatcher {

// Regular expression to support conditional tags like `plugin[artifactId='maven-compiler-plugin']` or foo[@bar='baz']
private static final Pattern PATTERN = Pattern.compile("([-\\w]+)\\[(@)?([-\\w]+)='([-\\w.]+)']");

Expand Down Expand Up @@ -67,6 +69,44 @@ public boolean matches(Cursor cursor) {
int pathIndex = 0;
for (int i = parts.length - 1; i >= 0; i--, pathIndex++) {
String part = parts[i];

String partWithCondition = null;
Xml.Tag tagForCondition = null;
if (part.endsWith("]") && i < path.size()) {
int index = part.indexOf("[");
if (index < 0) {
return false;
}
//if is Attribute
if (part.charAt(index + 1) == '@') {
partWithCondition = part;
tagForCondition = path.get(i);
}
} else if (i < path.size() && i > 0 && parts[i - 1].endsWith("]")) {
String partBefore = parts[i - 1];
int index = partBefore.indexOf("[");
if (index < 0) {
return false;
}
if (!partBefore.contains("@")) {
partWithCondition = partBefore;
tagForCondition = path.get(parts.length - i);
}
}

String partName;

Matcher matcher;
if (tagForCondition != null && partWithCondition.endsWith("]") && (matcher = PATTERN.matcher(partWithCondition)).matches()) {
String optionalPartName = matchesCondition(matcher, tagForCondition);
if (optionalPartName == null) {
return false;
}
partName = optionalPartName;
} else {
partName = null;
}

if (part.startsWith("@")) {
if (!(cursor.getValue() instanceof Xml.Attribute &&
(((Xml.Attribute) cursor.getValue()).getKeyAsString().equals(part.substring(1))) ||
Expand All @@ -78,7 +118,16 @@ public boolean matches(Cursor cursor) {
continue;
}

if (path.size() < i + 1 || (!path.get(pathIndex).getName().equals(part) && !"*".equals(part))) {
boolean conditionNotFulfilled =
tagForCondition == null || (!part.equals(partName) && !tagForCondition.getName()
.equals(partName));

int idx = part.indexOf("[");
if (idx > 0) {
part = part.substring(0, idx);
}
if (path.size() < i + 1 || (
!(path.get(pathIndex).getName().equals(part)) && !"*".equals(part)) && conditionNotFulfilled) {
return false;
}
}
Expand Down Expand Up @@ -124,20 +173,11 @@ public boolean matches(Cursor cursor) {

Matcher matcher;
if (tag != null && part.endsWith("]") && (matcher = PATTERN.matcher(part)).matches()) {
String name = matcher.group(1);
boolean isAttribute = Objects.equals(matcher.group(2), "@");
String selector = matcher.group(3);
String value = matcher.group(4);

boolean matchCondition = isAttribute ? tag.getAttributes().stream().anyMatch(a ->
a.getKeyAsString().equals(selector) && a.getValueAsString().equals(value)) :
FindTags.find(tag, selector).stream().anyMatch(t ->
t.getValue().map(v -> v.equals(value)).orElse(false)
);
if (!matchCondition) {
String optionalPartName = matchesCondition(matcher, tag);
if (optionalPartName == null) {
return false;
}
partName = name;
partName = optionalPartName;
} else {
partName = part;
}
Expand All @@ -156,4 +196,31 @@ public boolean matches(Cursor cursor) {
return cursor.getValue() instanceof Xml.Tag && path.size() == parts.length;
}
}

@Nullable
private String matchesCondition(Matcher matcher, Xml.Tag tag) {
String name = matcher.group(1);
boolean isAttribute = Objects.equals(matcher.group(2), "@");
String selector = matcher.group(3);
String value = matcher.group(4);

boolean matchCondition = false;
if (isAttribute) {
for (Xml.Attribute a : tag.getAttributes()) {
if (a.getKeyAsString().equals(selector) && a.getValueAsString().equals(value)) {
matchCondition = true;
break;
}
}
} else {
for (Xml.Tag t : FindTags.find(tag, selector)) {
if (t.getValue().map(v -> v.equals(value)).orElse(false)) {
matchCondition = true;
break;
}
}
}

return matchCondition ? name : null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ void matchPom() {
pomXml1)).isTrue();
assertThat(match("/project/build/plugins/plugin[artifactId='maven-compiler-plugin']/configuration/source",
pomXml1)).isTrue();
assertThat(match("//plugin[artifactId='maven-compiler-plugin']/configuration/source",
pomXml1)).isTrue();
assertThat(match("/project/build/plugins/plugin[groupId='org.apache.maven.plugins']/configuration/source",
pomXml1)).isTrue();
assertThat(match("/project/build/plugins/plugin[artifactId='somethingElse']/configuration/source",
Expand All @@ -173,6 +175,24 @@ void attributePredicate() {
assertThat(match("/root/element1[foo='baz']", xml)).isTrue();
}

@Test
void relativePathsWithConditions() {
SourceFile xml = new XmlParser().parse(
"""
<?xml version="1.0" encoding="UTF-8"?>
<root>
<element1 foo="bar">
<foo>baz</foo>
<test>asdf</test>
</element1>
</root>
"""
).toList().get(0);
assertThat(match("//element1[@foo='bar']", xml)).isTrue();
assertThat(match("//element1[foo='baz']/test", xml)).isTrue();
assertThat(match("//element1[foo='bar']/test", xml)).isFalse();
}

@Test
@Disabled
@Issue("https://github.com/openrewrite/rewrite/issues/3919")
Expand Down

0 comments on commit 4159303

Please sign in to comment.