-
Notifications
You must be signed in to change notification settings - Fork 366
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix all XPathMatcherTest TODO's + handle nested elements with the same name #4532
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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("]")) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don’t understand what’s the purpose of this |
||
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)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removing I don’t think the first condition is still relevant either, however the Note that the first condition is insufficient on its own because of URLs again – or actually any value that could contain double slashes in a condition. |
||
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); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using
i
straight up inpath.get()
was completely wrong sincepath
is reverted whileparts
is not –pathIndex
should always be used.Note that if a part starts with
@
, it MUST be the last part – I wanted to check it here, but then I realized allif
s actually result in the same code (and conditions matching sub-elements were not handled…).