Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 7a1b9c6

Browse files
ammachadotimtebeekknutwannhedenevie-laugithub-actions[bot]
authored
Partial support for parsing XML namespaces (openrewrite#3925)
* Partial support for parsing XML namespaces * Adding namespace resolution * Missing license header * Adding recipes to search namespace URIs/prefixes * Namespace shortcut methods on \'Xml.Document\' * Change implementation to rely only on attributes * Javadocs and cleanup * Rename XmlNamespaceUtils & minor polish Remove duplicate NonNull; see package-info.java Validate argument not literal Apply formatter * Fix namespace search on XML hierarchy * `ChangeNamespaceValue` now updates the `schemaLocation` attribute * Consider namespaces on `SemanticallyEqual`. * Suggestions from code review. * Update rewrite-xml/src/main/java/org/openrewrite/xml/ChangeNamespaceValue.java Co-authored-by: Knut Wannheden <[email protected]> * Revert namespace comparison changes in `SemanticallyEqual`. * Adding a Namespaces abstraction * Add support for wildcard and local-name() * Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Fix `Namespaces` mutability * Adding an iterator implementation for `Namespaces` * Replace `NotNull` with OpenRewrite's `NonNull` * Polish. Got rid of Namespaces class as it is mostly a thin wrapper around Map Merge XmlNamespaceUtils into Xml When you control the definition of the type creating a "utils" class for it makes those methods harder for users to discover than if they were defined on the class itself Moved unit test to use AssertJ assertions to be consistent with our other tests Removed Namespaces field from XPathMatcher because it was unused Sentence-cased recipe metadata --------- Co-authored-by: Knut Wannheden <[email protected]> Co-authored-by: Knut Wannheden <[email protected]> Co-authored-by: Evie Lau <[email protected]> Co-authored-by: Evie Lau <[email protected]> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Sam Snyder <[email protected]>
1 parent 02dc979 commit 7a1b9c6

File tree

11 files changed

+980
-50
lines changed

11 files changed

+980
-50
lines changed

rewrite-xml/src/main/java/org/openrewrite/xml/ChangeNamespaceValue.java

Lines changed: 160 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,26 @@
1919
import lombok.Value;
2020
import org.openrewrite.*;
2121
import org.openrewrite.internal.ListUtils;
22+
import org.openrewrite.internal.StringUtils;
2223
import org.openrewrite.internal.lang.Nullable;
24+
import org.openrewrite.marker.Markers;
2325
import org.openrewrite.xml.tree.Xml;
2426

27+
import java.util.Map;
28+
import java.util.Optional;
29+
import java.util.regex.Matcher;
30+
import java.util.regex.Pattern;
31+
32+
import static org.openrewrite.Tree.randomId;
33+
2534
@Value
2635
@EqualsAndHashCode(callSuper = false)
2736
public class ChangeNamespaceValue extends Recipe {
2837
private static final String XMLNS_PREFIX = "xmlns";
2938
private static final String VERSION_PREFIX = "version";
39+
private static final String SCHEMA_LOCATION_MATCH_PATTERN = "(?m)(.*)(%s)(\\s+)(.*)";
40+
private static final String SCHEMA_LOCATION_REPLACEMENT_PATTERN = "$1%s$3%s";
41+
private static final String MSG_TAG_UPDATED = "msg-tag-updated";
3042

3143
@Override
3244
public String getDisplayName() {
@@ -65,22 +77,73 @@ public String getDescription() {
6577
String versionMatcher;
6678

6779
@Nullable
68-
@Option(displayName = "Search All Namespaces",
80+
@Option(displayName = "Search all namespaces",
6981
description = "Specify whether evaluate all namespaces. Defaults to true",
7082
example = "true",
7183
required = false)
7284
Boolean searchAllNamespaces;
7385

86+
@Nullable
87+
@Option(displayName = "New Resource version",
88+
description = "The new version of the resource",
89+
example = "2.0")
90+
String newVersion;
91+
92+
@Option(displayName = "Schema location",
93+
description = "The new value to be used for the namespace schema location.",
94+
example = "newfoo.bar.attribute.value.string",
95+
required = false)
96+
@Nullable
97+
String newSchemaLocation;
98+
99+
public static final String XML_SCHEMA_INSTANCE_PREFIX = "xsi";
100+
public static final String XML_SCHEMA_INSTANCE_URI = "http://www.w3.org/2001/XMLSchema-instance";
101+
102+
/**
103+
* Find the tag that contains the declaration of the {@link #XML_SCHEMA_INSTANCE_URI} namespace.
104+
*
105+
* @param cursor the cursor to search from
106+
* @return the tag that contains the declaration of the given namespace URI.
107+
*/
108+
public static Xml.Tag findTagContainingXmlSchemaInstanceNamespace(Cursor cursor) {
109+
while (cursor != null) {
110+
if (cursor.getValue() instanceof Xml.Document) {
111+
return ((Xml.Document) cursor.getValue()).getRoot();
112+
}
113+
Xml.Tag tag = cursor.firstEnclosing(Xml.Tag.class);
114+
if (tag != null) {
115+
if (tag.getNamespaces().containsValue(XML_SCHEMA_INSTANCE_URI)) {
116+
return tag;
117+
}
118+
}
119+
cursor = cursor.getParent();
120+
}
121+
122+
// Should never happen
123+
throw new IllegalArgumentException("Could not find tag containing namespace '" + XML_SCHEMA_INSTANCE_URI + "' or the enclosing Xml.Document instance.");
124+
}
125+
74126
@Override
75127
public TreeVisitor<?, ExecutionContext> getVisitor() {
76128
XPathMatcher elementNameMatcher = elementName != null ? new XPathMatcher(elementName) : null;
77129
return new XmlIsoVisitor<ExecutionContext>() {
130+
@Override
131+
public Xml.Document visitDocument(Xml.Document document, ExecutionContext ctx) {
132+
Xml.Document d = super.visitDocument(document, ctx);
133+
if (ctx.pollMessage(MSG_TAG_UPDATED, false)) {
134+
d = d.withRoot(addOrUpdateSchemaLocation(d.getRoot(), getCursor()));
135+
}
136+
return d;
137+
}
138+
78139
@Override
79140
public Xml.Tag visitTag(Xml.Tag tag, ExecutionContext ctx) {
80141
Xml.Tag t = super.visitTag(tag, ctx);
81142

82143
if (matchesElementName(getCursor()) && matchesVersion(t)) {
83144
t = t.withAttributes(ListUtils.map(t.getAttributes(), this::maybeReplaceNamespaceAttribute));
145+
t = t.withAttributes(ListUtils.map(t.getAttributes(), this::maybeReplaceVersionAttribute));
146+
ctx.putMessage(MSG_TAG_UPDATED, true);
84147
}
85148

86149
return t;
@@ -114,6 +177,18 @@ private Xml.Attribute maybeReplaceNamespaceAttribute(Xml.Attribute attribute) {
114177
return attribute;
115178
}
116179

180+
private Xml.Attribute maybeReplaceVersionAttribute(Xml.Attribute attribute) {
181+
if (isVersionAttribute(attribute) && newVersion != null) {
182+
return attribute.withValue(
183+
new Xml.Attribute.Value(attribute.getId(),
184+
"",
185+
attribute.getMarkers(),
186+
attribute.getValue().getQuote(),
187+
newVersion));
188+
}
189+
return attribute;
190+
}
191+
117192
private boolean isXmlnsAttribute(Xml.Attribute attribute) {
118193
boolean searchAll = searchAllNamespaces == null || Boolean.TRUE.equals(searchAllNamespaces);
119194
return searchAll && attribute.getKeyAsString().startsWith(XMLNS_PREFIX) ||
@@ -129,6 +204,9 @@ private boolean isOldValue(Xml.Attribute attribute) {
129204
}
130205

131206
private boolean isVersionMatch(Xml.Attribute attribute) {
207+
if (versionMatcher == null) {
208+
return true;
209+
}
132210
String[] versions = versionMatcher.split(",");
133211
double dversion = Double.parseDouble(attribute.getValueAsString());
134212
for (String splitVersion : versions) {
@@ -149,6 +227,87 @@ private boolean isVersionMatch(Xml.Attribute attribute) {
149227
}
150228
return false;
151229
}
230+
231+
private Xml.Tag addOrUpdateSchemaLocation(Xml.Tag root, Cursor cursor) {
232+
if (StringUtils.isBlank(newSchemaLocation)) {
233+
return root;
234+
}
235+
Xml.Tag newRoot = maybeAddNamespace(root);
236+
Optional<Xml.Attribute> maybeSchemaLocation = maybeGetSchemaLocation(cursor, newRoot);
237+
if (maybeSchemaLocation.isPresent() && oldValue != null) {
238+
newRoot = updateSchemaLocation(newRoot, maybeSchemaLocation.get());
239+
} else if (!maybeSchemaLocation.isPresent()) {
240+
newRoot = addSchemaLocation(newRoot);
241+
}
242+
return newRoot;
243+
}
244+
245+
private Optional<Xml.Attribute> maybeGetSchemaLocation(Cursor cursor, Xml.Tag tag) {
246+
Xml.Tag schemaLocationTag = findTagContainingXmlSchemaInstanceNamespace(cursor);
247+
Map<String, String> namespaces = tag.getNamespaces();
248+
for (Xml.Attribute attribute : schemaLocationTag.getAttributes()) {
249+
String attributeNamespace = namespaces.get(Xml.extractNamespacePrefix(attribute.getKeyAsString()));
250+
if(XML_SCHEMA_INSTANCE_URI.equals(attributeNamespace)
251+
&& attribute.getKeyAsString().endsWith("schemaLocation")) {
252+
return Optional.of(attribute);
253+
}
254+
}
255+
256+
return Optional.empty();
257+
}
258+
259+
private Xml.Tag maybeAddNamespace(Xml.Tag root) {
260+
Map<String, String> namespaces = root.getNamespaces();
261+
if (namespaces.containsValue(newValue) && !namespaces.containsValue(XML_SCHEMA_INSTANCE_URI)) {
262+
namespaces.put(XML_SCHEMA_INSTANCE_PREFIX, XML_SCHEMA_INSTANCE_URI);
263+
root = root.withNamespaces(namespaces);
264+
}
265+
return root;
266+
}
267+
268+
private Xml.Tag updateSchemaLocation(Xml.Tag newRoot, Xml.Attribute attribute) {
269+
if(oldValue == null) {
270+
return newRoot;
271+
}
272+
String oldSchemaLocation = attribute.getValueAsString();
273+
Matcher pattern = Pattern.compile(String.format(SCHEMA_LOCATION_MATCH_PATTERN, Pattern.quote(oldValue)))
274+
.matcher(oldSchemaLocation);
275+
if (pattern.find()) {
276+
String newSchemaLocationValue = pattern.replaceFirst(
277+
String.format(SCHEMA_LOCATION_REPLACEMENT_PATTERN, newValue, newSchemaLocation)
278+
);
279+
Xml.Attribute newAttribute = attribute.withValue(attribute.getValue().withValue(newSchemaLocationValue));
280+
newRoot = newRoot.withAttributes(ListUtils.map(newRoot.getAttributes(), a -> a == attribute ? newAttribute : a));
281+
}
282+
return newRoot;
283+
}
284+
285+
private Xml.Tag addSchemaLocation(Xml.Tag newRoot) {
286+
return newRoot.withAttributes(
287+
ListUtils.concat(
288+
newRoot.getAttributes(),
289+
new Xml.Attribute(
290+
randomId(),
291+
" ",
292+
Markers.EMPTY,
293+
new Xml.Ident(
294+
randomId(),
295+
"",
296+
Markers.EMPTY,
297+
String.format("%s:schemaLocation", XML_SCHEMA_INSTANCE_PREFIX)
298+
),
299+
"",
300+
new Xml.Attribute.Value(
301+
randomId(),
302+
"",
303+
Markers.EMPTY,
304+
Xml.Attribute.Value.Quote.Double,
305+
String.format("%s %s", newValue, newSchemaLocation)
306+
)
307+
)
308+
)
309+
);
310+
}
152311
};
153312
}
154313
}

rewrite-xml/src/main/java/org/openrewrite/xml/XPathMatcher.java

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
public class XPathMatcher {
3838

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

4242
private final String expression;
4343
private final boolean startsWithSlash;
@@ -82,6 +82,9 @@ public boolean matches(Cursor cursor) {
8282
if (part.charAt(index + 1) == '@') {
8383
partWithCondition = part;
8484
tagForCondition = path.get(i);
85+
} else if (part.contains("(") && part.contains(")")) { //if is function
86+
partWithCondition = part;
87+
tagForCondition = path.get(i);
8588
}
8689
} else if (i < path.size() && i > 0 && parts[i - 1].endsWith("]")) {
8790
String partBefore = parts[i - 1];
@@ -94,14 +97,16 @@ public boolean matches(Cursor cursor) {
9497
partWithCondition = partBefore;
9598
tagForCondition = path.get(parts.length - i);
9699
}
100+
} else if (part.endsWith(")")) { // is xpath method
101+
// TODO: implement other xpath methods
97102
}
98103

99104
String partName;
100105

101106
Matcher matcher;
102107
if (tagForCondition != null && partWithCondition.endsWith("]") && (matcher = PATTERN.matcher(
103108
partWithCondition)).matches()) {
104-
String optionalPartName = matchesCondition(matcher, tagForCondition);
109+
String optionalPartName = matchesCondition(matcher, tagForCondition, cursor);
105110
if (optionalPartName == null) {
106111
return false;
107112
}
@@ -176,7 +181,7 @@ public boolean matches(Cursor cursor) {
176181

177182
Matcher matcher;
178183
if (tag != null && part.endsWith("]") && (matcher = PATTERN.matcher(part)).matches()) {
179-
String optionalPartName = matchesCondition(matcher, tag);
184+
String optionalPartName = matchesCondition(matcher, tag, cursor);
180185
if (optionalPartName == null) {
181186
return false;
182187
}
@@ -191,7 +196,7 @@ public boolean matches(Cursor cursor) {
191196
"*".equals(part.substring(1)));
192197
}
193198

194-
if (path.size() < i + 1 || (tag != null && !tag.getName().equals(partName) && !"*".equals(part))) {
199+
if (path.size() < i + 1 || (tag != null && !tag.getName().equals(partName) && !partName.equals("*") && !"*".equals(part))) {
195200
return false;
196201
}
197202
}
@@ -201,21 +206,34 @@ public boolean matches(Cursor cursor) {
201206
}
202207

203208
@Nullable
204-
private String matchesCondition(Matcher matcher, Xml.Tag tag) {
209+
private String matchesCondition(Matcher matcher, Xml.Tag tag, Cursor cursor) {
205210
String name = matcher.group(1);
206-
boolean isAttribute = Objects.equals(matcher.group(2), "@");
207-
String selector = matcher.group(3);
208-
String value = matcher.group(4);
211+
boolean isAttribute = matcher.group(4) != null; // either group4 != null, or group 2 startsWith @
212+
String selector = isAttribute ? matcher.group(5) : matcher.group(2);
213+
boolean isFunction = selector.endsWith("()");
214+
String value = matcher.group(6);
209215

210216
boolean matchCondition = false;
211217
if (isAttribute) {
212218
for (Xml.Attribute a : tag.getAttributes()) {
213-
if (a.getKeyAsString().equals(selector) && a.getValueAsString().equals(value)) {
219+
if ((a.getKeyAsString().equals(selector) || "*".equals(selector)) && a.getValueAsString().equals(value)) {
214220
matchCondition = true;
215221
break;
216222
}
217223
}
218-
} else {
224+
} else if (isFunction) {
225+
if (!name.equals("*") && !tag.getLocalName().equals(name)) {
226+
matchCondition = false;
227+
} else if (selector.equals("local-name()")) {
228+
if (tag.getLocalName().equals(value)) {
229+
matchCondition = true;
230+
}
231+
} else if (selector.equals("namespace-uri()")) {
232+
if (tag.getNamespaceUri(cursor).get().equals(value)) {
233+
matchCondition = true;
234+
}
235+
}
236+
} else { // other [] conditions
219237
for (Xml.Tag t : FindTags.find(tag, selector)) {
220238
if (t.getValue().map(v -> v.equals(value)).orElse(false)) {
221239
matchCondition = true;

0 commit comments

Comments
 (0)