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 bae47c1b672..74905d9281e 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/XPathMatcher.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/XPathMatcher.java @@ -29,6 +29,7 @@ /** * Supports a limited set of XPath expressions, specifically those documented on this page. + * Additionally, supports `local-name()` and `namespace-uri()` conditions, `and`/`or` operators, and chained conditions. *
* Used for checking whether a visitor's cursor meets a certain XPath expression. *
@@ -37,9 +38,11 @@
*/
public class XPathMatcher {
- private static final Pattern XPATH_ELEMENT_SPLITTER = Pattern.compile("((?<=/)(?=/)|[^/\\[]|\\[[^]]*\\])+");
+ private static final Pattern XPATH_ELEMENT_SPLITTER = Pattern.compile("((?<=/)(?=/)|[^/\\[]|\\[[^]]*])+");
// Regular expression to support conditional tags like `plugin[artifactId='maven-compiler-plugin']` or foo[@bar='baz']
- private static final Pattern PATTERN = Pattern.compile("(@)?([-:\\w]+|\\*)\\[((local-name|namespace-uri)\\(\\)|(@)?([-\\w]+|\\*))='(.*)']");
+ private static final Pattern ELEMENT_WITH_CONDITION_PATTERN = Pattern.compile("(@)?([-:\\w]+|\\*)(\\[.+])");
+ private static final Pattern CONDITION_PATTERN = Pattern.compile("(\\[.*?])+?");
+ private static final Pattern CONDITION_CONJUNCTION_PATTERN = Pattern.compile("(((local-name|namespace-uri)\\(\\)|(@)?([-\\w:]+|\\*))='(.*?)'(\\h?(or|and)\\h?)?)+?");
private final String expression;
private final boolean startsWithSlash;
@@ -120,8 +123,8 @@ public boolean matches(Cursor cursor) {
boolean matchedCondition = false;
Matcher matcher;
- if (tagForCondition != null && partWithCondition.endsWith("]") && (matcher = PATTERN.matcher(
- partWithCondition)).matches()) {
+ if (tagForCondition != null && partWithCondition.endsWith("]")
+ && (matcher = ELEMENT_WITH_CONDITION_PATTERN.matcher(partWithCondition)).matches()) {
String optionalPartName = matchesElementWithConditionFunction(matcher, tagForCondition, cursor);
if (optionalPartName == null) {
return false;
@@ -147,16 +150,16 @@ public boolean matches(Cursor cursor) {
continue;
}
- boolean conditionNotFulfilled =
- tagForCondition == null || (!part.equals(partName) && !tagForCondition.getName()
- .equals(partName));
+ 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)) || conditionIsBefore && conditionNotFulfilled) {
+ if (path.size() < i + 1
+ || (!(path.get(pathIndex).getName().equals(part)) && !"*".equals(part))
+ || conditionIsBefore && conditionNotFulfilled) {
return false;
}
}
@@ -203,7 +206,7 @@ public boolean matches(Cursor cursor) {
boolean matchedCondition = false;
Matcher matcher;
- if (tag != null && part.endsWith("]") && (matcher = PATTERN.matcher(part)).matches()) {
+ if (tag != null && part.endsWith("]") && (matcher = ELEMENT_WITH_CONDITION_PATTERN.matcher(part)).matches()) {
String optionalPartName = matchesElementWithConditionFunction(matcher, tag, cursor);
if (optionalPartName == null) {
return false;
@@ -236,40 +239,74 @@ public boolean matches(Cursor cursor) {
private String matchesElementWithConditionFunction(Matcher matcher, Xml.Tag tag, Cursor cursor) {
boolean isAttributeElement = matcher.group(1) != null;
String element = matcher.group(2);
- boolean isAttributeCondition = matcher.group(5) != null; // either group4 != null, or group 2 startsWith @
- String selector = isAttributeCondition ? matcher.group(6) : matcher.group(3);
- boolean isFunctionCondition = selector.endsWith("()");
- String value = matcher.group(7);
-
- boolean matchCondition = false;
- if (isAttributeCondition) {
- for (Xml.Attribute a : tag.getAttributes()) {
- if ((a.getKeyAsString().equals(selector) || "*".equals(selector)) && a.getValueAsString().equals(value)) {
- matchCondition = true;
+ String allConditions = matcher.group(3);
+
+ // Fail quickly if element name doesn't match
+ if (!isAttributeElement && !tag.getName().equals(element) && !"*".equals(element)) {
+ return null;
+ }
+
+ // check that all conditions match on current element
+ Matcher conditions = CONDITION_PATTERN.matcher(allConditions);
+ boolean stillMatchesConditions = true;
+ while (conditions.find() && stillMatchesConditions) {
+ String conditionGroup = conditions.group(1);
+ Matcher condition = CONDITION_CONJUNCTION_PATTERN.matcher(conditionGroup);
+ boolean orCondition = false;
+
+ while (condition.find() && (stillMatchesConditions || orCondition)) {
+ boolean matchCurrentCondition = false;
+
+ boolean isAttributeCondition = condition.group(4) != null;
+ String selector = isAttributeCondition ? condition.group(5) : condition.group(2);
+ boolean isFunctionCondition = selector.endsWith("()");
+ String value = condition.group(6);
+ String conjunction = condition.group(8);
+ orCondition = conjunction != null && conjunction.equals("or");
+
+ // invalid conjunction if not 'or' or 'and'
+ if (!orCondition && conjunction != null && !conjunction.equals("and")) {
+ // TODO: throw exception for invalid or unsupported XPath conjunction?
+ stillMatchesConditions = false;
break;
}
- }
- } else if (isFunctionCondition) {
- if (isAttributeElement) {
- for (Xml.Attribute a : tag.getAttributes()) {
- if (matchesElementAndFunction(a, cursor, element, selector, value)) {
- matchCondition = true;
- break;
+
+ if (isAttributeCondition) { // [@attr='value'] pattern
+ for (Xml.Attribute a : tag.getAttributes()) {
+ if ((a.getKeyAsString().equals(selector) || "*".equals(selector)) && a.getValueAsString().equals(value)) {
+ matchCurrentCondition = true;
+ break;
+ }
+ }
+ } else if (isFunctionCondition) { // [local-name()='name'] pattern
+ if (isAttributeElement) {
+ for (Xml.Attribute a : tag.getAttributes()) {
+ if (matchesElementAndFunction(a, cursor, element, selector, value)) {
+ matchCurrentCondition = true;
+ break;
+ }
+ }
+ } else {
+ matchCurrentCondition = matchesElementAndFunction(tag, cursor, element, selector, value);
+ }
+ } else { // other [] conditions
+ for (Xml.Tag t : FindTags.find(tag, selector)) {
+ if (t.getValue().map(v -> v.equals(value)).orElse(false)) {
+ matchCurrentCondition = true;
+ break;
+ }
}
}
- } else {
- matchCondition = matchesElementAndFunction(tag, cursor, element, selector, value);
- }
- } else { // other [] conditions
- for (Xml.Tag t : FindTags.find(tag, selector)) {
- if (t.getValue().map(v -> v.equals(value)).orElse(false)) {
- matchCondition = true;
+ // break condition early if first OR condition is fulfilled
+ if (matchCurrentCondition && orCondition) {
break;
}
+
+ stillMatchesConditions = matchCurrentCondition;
}
}
- return matchCondition ? element : null;
+ return stillMatchesConditions ? element : null;
}
private static boolean matchesElementAndFunction(Namespaced tagOrAttribute, Cursor cursor, String element, String selector, String value) {
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 04bee0758b2..f369bfda2c2 100755
--- a/rewrite-xml/src/test/java/org/openrewrite/xml/XPathMatcherTest.java
+++ b/rewrite-xml/src/test/java/org/openrewrite/xml/XPathMatcherTest.java
@@ -107,6 +107,7 @@ class XPathMatcherTest {