From 72e64aba26b3267e193ccf60391906636f64ef9d Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Thu, 20 Jun 2024 00:50:56 -0400 Subject: [PATCH] Partial support for parsing XML namespaces (#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 * 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 Co-authored-by: Knut Wannheden Co-authored-by: Evie Lau <689163+evie-lau@users.noreply.github.com> Co-authored-by: Evie Lau Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Sam Snyder --- .../openrewrite/xml/ChangeNamespaceValue.java | 161 ++++++++++++- .../org/openrewrite/xml/XPathMatcher.java | 38 +++- .../xml/search/FindNamespacePrefix.java | 90 ++++++++ .../xml/search/HasNamespaceUri.java | 90 ++++++++ .../java/org/openrewrite/xml/tree/Xml.java | 214 ++++++++++++++++-- .../xml/ChangeNamespaceValueTest.java | 64 +++++- .../openrewrite/xml/CreateXmlFileTest.java | 2 + .../org/openrewrite/xml/XPathMatcherTest.java | 68 ++++-- .../org/openrewrite/xml/internal/XmlTest.java | 57 +++++ .../xml/search/FindNamespacePrefixTest.java | 123 ++++++++++ .../xml/search/HasNamespaceUriTest.java | 123 ++++++++++ 11 files changed, 980 insertions(+), 50 deletions(-) create mode 100644 rewrite-xml/src/main/java/org/openrewrite/xml/search/FindNamespacePrefix.java create mode 100644 rewrite-xml/src/main/java/org/openrewrite/xml/search/HasNamespaceUri.java create mode 100644 rewrite-xml/src/test/java/org/openrewrite/xml/internal/XmlTest.java create mode 100644 rewrite-xml/src/test/java/org/openrewrite/xml/search/FindNamespacePrefixTest.java create mode 100644 rewrite-xml/src/test/java/org/openrewrite/xml/search/HasNamespaceUriTest.java diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/ChangeNamespaceValue.java b/rewrite-xml/src/main/java/org/openrewrite/xml/ChangeNamespaceValue.java index b821a95421e..6813ed343f2 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/ChangeNamespaceValue.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/ChangeNamespaceValue.java @@ -19,14 +19,26 @@ import lombok.Value; import org.openrewrite.*; import org.openrewrite.internal.ListUtils; +import org.openrewrite.internal.StringUtils; import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.marker.Markers; import org.openrewrite.xml.tree.Xml; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.openrewrite.Tree.randomId; + @Value @EqualsAndHashCode(callSuper = false) public class ChangeNamespaceValue extends Recipe { private static final String XMLNS_PREFIX = "xmlns"; private static final String VERSION_PREFIX = "version"; + private static final String SCHEMA_LOCATION_MATCH_PATTERN = "(?m)(.*)(%s)(\\s+)(.*)"; + private static final String SCHEMA_LOCATION_REPLACEMENT_PATTERN = "$1%s$3%s"; + private static final String MSG_TAG_UPDATED = "msg-tag-updated"; @Override public String getDisplayName() { @@ -65,22 +77,73 @@ public String getDescription() { String versionMatcher; @Nullable - @Option(displayName = "Search All Namespaces", + @Option(displayName = "Search all namespaces", description = "Specify whether evaluate all namespaces. Defaults to true", example = "true", required = false) Boolean searchAllNamespaces; + @Nullable + @Option(displayName = "New Resource version", + description = "The new version of the resource", + example = "2.0") + String newVersion; + + @Option(displayName = "Schema location", + description = "The new value to be used for the namespace schema location.", + example = "newfoo.bar.attribute.value.string", + required = false) + @Nullable + String newSchemaLocation; + + public static final String XML_SCHEMA_INSTANCE_PREFIX = "xsi"; + public static final String XML_SCHEMA_INSTANCE_URI = "http://www.w3.org/2001/XMLSchema-instance"; + + /** + * Find the tag that contains the declaration of the {@link #XML_SCHEMA_INSTANCE_URI} namespace. + * + * @param cursor the cursor to search from + * @return the tag that contains the declaration of the given namespace URI. + */ + public static Xml.Tag findTagContainingXmlSchemaInstanceNamespace(Cursor cursor) { + while (cursor != null) { + if (cursor.getValue() instanceof Xml.Document) { + return ((Xml.Document) cursor.getValue()).getRoot(); + } + Xml.Tag tag = cursor.firstEnclosing(Xml.Tag.class); + if (tag != null) { + if (tag.getNamespaces().containsValue(XML_SCHEMA_INSTANCE_URI)) { + return tag; + } + } + cursor = cursor.getParent(); + } + + // Should never happen + throw new IllegalArgumentException("Could not find tag containing namespace '" + XML_SCHEMA_INSTANCE_URI + "' or the enclosing Xml.Document instance."); + } + @Override public TreeVisitor getVisitor() { XPathMatcher elementNameMatcher = elementName != null ? new XPathMatcher(elementName) : null; return new XmlIsoVisitor() { + @Override + public Xml.Document visitDocument(Xml.Document document, ExecutionContext ctx) { + Xml.Document d = super.visitDocument(document, ctx); + if (ctx.pollMessage(MSG_TAG_UPDATED, false)) { + d = d.withRoot(addOrUpdateSchemaLocation(d.getRoot(), getCursor())); + } + return d; + } + @Override public Xml.Tag visitTag(Xml.Tag tag, ExecutionContext ctx) { Xml.Tag t = super.visitTag(tag, ctx); if (matchesElementName(getCursor()) && matchesVersion(t)) { t = t.withAttributes(ListUtils.map(t.getAttributes(), this::maybeReplaceNamespaceAttribute)); + t = t.withAttributes(ListUtils.map(t.getAttributes(), this::maybeReplaceVersionAttribute)); + ctx.putMessage(MSG_TAG_UPDATED, true); } return t; @@ -114,6 +177,18 @@ private Xml.Attribute maybeReplaceNamespaceAttribute(Xml.Attribute attribute) { return attribute; } + private Xml.Attribute maybeReplaceVersionAttribute(Xml.Attribute attribute) { + if (isVersionAttribute(attribute) && newVersion != null) { + return attribute.withValue( + new Xml.Attribute.Value(attribute.getId(), + "", + attribute.getMarkers(), + attribute.getValue().getQuote(), + newVersion)); + } + return attribute; + } + private boolean isXmlnsAttribute(Xml.Attribute attribute) { boolean searchAll = searchAllNamespaces == null || Boolean.TRUE.equals(searchAllNamespaces); return searchAll && attribute.getKeyAsString().startsWith(XMLNS_PREFIX) || @@ -129,6 +204,9 @@ private boolean isOldValue(Xml.Attribute attribute) { } private boolean isVersionMatch(Xml.Attribute attribute) { + if (versionMatcher == null) { + return true; + } String[] versions = versionMatcher.split(","); double dversion = Double.parseDouble(attribute.getValueAsString()); for (String splitVersion : versions) { @@ -149,6 +227,87 @@ private boolean isVersionMatch(Xml.Attribute attribute) { } return false; } + + private Xml.Tag addOrUpdateSchemaLocation(Xml.Tag root, Cursor cursor) { + if (StringUtils.isBlank(newSchemaLocation)) { + return root; + } + Xml.Tag newRoot = maybeAddNamespace(root); + Optional maybeSchemaLocation = maybeGetSchemaLocation(cursor, newRoot); + if (maybeSchemaLocation.isPresent() && oldValue != null) { + newRoot = updateSchemaLocation(newRoot, maybeSchemaLocation.get()); + } else if (!maybeSchemaLocation.isPresent()) { + newRoot = addSchemaLocation(newRoot); + } + return newRoot; + } + + private Optional maybeGetSchemaLocation(Cursor cursor, Xml.Tag tag) { + Xml.Tag schemaLocationTag = findTagContainingXmlSchemaInstanceNamespace(cursor); + Map namespaces = tag.getNamespaces(); + for (Xml.Attribute attribute : schemaLocationTag.getAttributes()) { + String attributeNamespace = namespaces.get(Xml.extractNamespacePrefix(attribute.getKeyAsString())); + if(XML_SCHEMA_INSTANCE_URI.equals(attributeNamespace) + && attribute.getKeyAsString().endsWith("schemaLocation")) { + return Optional.of(attribute); + } + } + + return Optional.empty(); + } + + private Xml.Tag maybeAddNamespace(Xml.Tag root) { + Map namespaces = root.getNamespaces(); + if (namespaces.containsValue(newValue) && !namespaces.containsValue(XML_SCHEMA_INSTANCE_URI)) { + namespaces.put(XML_SCHEMA_INSTANCE_PREFIX, XML_SCHEMA_INSTANCE_URI); + root = root.withNamespaces(namespaces); + } + return root; + } + + private Xml.Tag updateSchemaLocation(Xml.Tag newRoot, Xml.Attribute attribute) { + if(oldValue == null) { + return newRoot; + } + String oldSchemaLocation = attribute.getValueAsString(); + Matcher pattern = Pattern.compile(String.format(SCHEMA_LOCATION_MATCH_PATTERN, Pattern.quote(oldValue))) + .matcher(oldSchemaLocation); + if (pattern.find()) { + String newSchemaLocationValue = pattern.replaceFirst( + String.format(SCHEMA_LOCATION_REPLACEMENT_PATTERN, newValue, newSchemaLocation) + ); + Xml.Attribute newAttribute = attribute.withValue(attribute.getValue().withValue(newSchemaLocationValue)); + newRoot = newRoot.withAttributes(ListUtils.map(newRoot.getAttributes(), a -> a == attribute ? newAttribute : a)); + } + return newRoot; + } + + private Xml.Tag addSchemaLocation(Xml.Tag newRoot) { + return newRoot.withAttributes( + ListUtils.concat( + newRoot.getAttributes(), + new Xml.Attribute( + randomId(), + " ", + Markers.EMPTY, + new Xml.Ident( + randomId(), + "", + Markers.EMPTY, + String.format("%s:schemaLocation", XML_SCHEMA_INSTANCE_PREFIX) + ), + "", + new Xml.Attribute.Value( + randomId(), + "", + Markers.EMPTY, + Xml.Attribute.Value.Quote.Double, + String.format("%s %s", newValue, newSchemaLocation) + ) + ) + ) + ); + } }; } } 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 9aa28ad3ec2..7c6066b72f9 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/XPathMatcher.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/XPathMatcher.java @@ -37,7 +37,7 @@ 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.]+)']"); + private static final Pattern PATTERN = Pattern.compile("([-\\w]+|\\*)\\[((local-name|namespace-uri)\\(\\)|(@)?([-\\w]+|\\*))='([-\\w.]+)']"); private final String expression; private final boolean startsWithSlash; @@ -82,6 +82,9 @@ public boolean matches(Cursor cursor) { if (part.charAt(index + 1) == '@') { partWithCondition = part; tagForCondition = path.get(i); + } else if (part.contains("(") && part.contains(")")) { //if is function + partWithCondition = part; + tagForCondition = path.get(i); } } else if (i < path.size() && i > 0 && parts[i - 1].endsWith("]")) { String partBefore = parts[i - 1]; @@ -94,6 +97,8 @@ public boolean matches(Cursor cursor) { partWithCondition = partBefore; tagForCondition = path.get(parts.length - i); } + } else if (part.endsWith(")")) { // is xpath method + // TODO: implement other xpath methods } String partName; @@ -101,7 +106,7 @@ public boolean matches(Cursor cursor) { Matcher matcher; if (tagForCondition != null && partWithCondition.endsWith("]") && (matcher = PATTERN.matcher( partWithCondition)).matches()) { - String optionalPartName = matchesCondition(matcher, tagForCondition); + String optionalPartName = matchesCondition(matcher, tagForCondition, cursor); if (optionalPartName == null) { return false; } @@ -176,7 +181,7 @@ public boolean matches(Cursor cursor) { Matcher matcher; if (tag != null && part.endsWith("]") && (matcher = PATTERN.matcher(part)).matches()) { - String optionalPartName = matchesCondition(matcher, tag); + String optionalPartName = matchesCondition(matcher, tag, cursor); if (optionalPartName == null) { return false; } @@ -191,7 +196,7 @@ public boolean matches(Cursor cursor) { "*".equals(part.substring(1))); } - if (path.size() < i + 1 || (tag != null && !tag.getName().equals(partName) && !"*".equals(part))) { + if (path.size() < i + 1 || (tag != null && !tag.getName().equals(partName) && !partName.equals("*") && !"*".equals(part))) { return false; } } @@ -201,21 +206,34 @@ public boolean matches(Cursor cursor) { } @Nullable - private String matchesCondition(Matcher matcher, Xml.Tag tag) { + private String matchesCondition(Matcher matcher, Xml.Tag tag, Cursor cursor) { String name = matcher.group(1); - boolean isAttribute = Objects.equals(matcher.group(2), "@"); - String selector = matcher.group(3); - String value = matcher.group(4); + boolean isAttribute = matcher.group(4) != null; // either group4 != null, or group 2 startsWith @ + String selector = isAttribute ? matcher.group(5) : matcher.group(2); + boolean isFunction = selector.endsWith("()"); + String value = matcher.group(6); boolean matchCondition = false; if (isAttribute) { for (Xml.Attribute a : tag.getAttributes()) { - if (a.getKeyAsString().equals(selector) && a.getValueAsString().equals(value)) { + if ((a.getKeyAsString().equals(selector) || "*".equals(selector)) && a.getValueAsString().equals(value)) { matchCondition = true; break; } } - } else { + } else if (isFunction) { + if (!name.equals("*") && !tag.getLocalName().equals(name)) { + matchCondition = false; + } else if (selector.equals("local-name()")) { + if (tag.getLocalName().equals(value)) { + matchCondition = true; + } + } else if (selector.equals("namespace-uri()")) { + if (tag.getNamespaceUri(cursor).get().equals(value)) { + matchCondition = true; + } + } + } else { // other [] conditions for (Xml.Tag t : FindTags.find(tag, selector)) { if (t.getValue().map(v -> v.equals(value)).orElse(false)) { matchCondition = true; diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/search/FindNamespacePrefix.java b/rewrite-xml/src/main/java/org/openrewrite/xml/search/FindNamespacePrefix.java new file mode 100644 index 00000000000..95623523636 --- /dev/null +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/search/FindNamespacePrefix.java @@ -0,0 +1,90 @@ +/* + * 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.search; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Option; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.internal.StringUtils; +import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.marker.SearchResult; +import org.openrewrite.xml.XPathMatcher; +import org.openrewrite.xml.XmlVisitor; +import org.openrewrite.xml.tree.Xml; + +import java.util.HashSet; +import java.util.Set; + +@Value +@EqualsAndHashCode(callSuper = false) +public class FindNamespacePrefix extends Recipe { + + @Option(displayName = "Namespace prefix", + description = "The Namespace Prefix to find.", + example = "http://www.w3.org/2001/XMLSchema-instance") + String namespacePrefix; + + @Option(displayName = "XPath", + description = "An XPath expression used to find namespace URIs.", + example = "/dependencies/dependency", + required = false) + @Nullable + String xPath; + + @Override + public String getDisplayName() { + return "Find XML namespace prefixes"; + } + + @Override + public String getDescription() { + return "Find XML namespace prefixes, optionally restricting the search by a XPath expression."; + } + + @Override + public TreeVisitor getVisitor() { + XPathMatcher matcher = StringUtils.isBlank(xPath) ? null : new XPathMatcher(xPath); + return new XmlVisitor() { + + @Override + public Xml visitTag(Xml.Tag tag, ExecutionContext ctx) { + Xml.Tag t = (Xml.Tag) super.visitTag(tag, ctx); + if (tag.getNamespaces().containsKey(namespacePrefix) && (matcher == null || matcher.matches(getCursor()))) { + t = SearchResult.found(t); + } + return t; + } + }; + } + + public static Set find(Xml x, String namespacePrefix, @Nullable String xPath) { + XPathMatcher matcher = StringUtils.isBlank(xPath) ? null : new XPathMatcher(xPath); + Set ts = new HashSet<>(); + new XmlVisitor>() { + @Override + public Xml visitTag(Xml.Tag tag, Set ts) { + if (tag.getNamespaces().containsKey(namespacePrefix) && (matcher == null || matcher.matches(getCursor()))) { + ts.add(tag); + } + return super.visitTag(tag, ts); + } + }.visit(x, ts); + return ts; + } +} diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/search/HasNamespaceUri.java b/rewrite-xml/src/main/java/org/openrewrite/xml/search/HasNamespaceUri.java new file mode 100644 index 00000000000..2745aff2264 --- /dev/null +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/search/HasNamespaceUri.java @@ -0,0 +1,90 @@ +/* + * 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.search; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Option; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.internal.StringUtils; +import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.marker.SearchResult; +import org.openrewrite.xml.XPathMatcher; +import org.openrewrite.xml.XmlVisitor; +import org.openrewrite.xml.tree.Xml; + +import java.util.HashSet; +import java.util.Set; + +@Value +@EqualsAndHashCode(callSuper = false) +public class HasNamespaceUri extends Recipe { + + @Option(displayName = "Namespace URI", + description = "The Namespace URI to find.", + example = "http://www.w3.org/2001/XMLSchema-instance") + String namespaceUri; + + @Option(displayName = "XPath", + description = "An XPath expression used to find namespace URIs.", + example = "/dependencies/dependency", + required = false) + @Nullable + String xPath; + + @Override + public String getDisplayName() { + return "Find XML namespace URIs"; + } + + @Override + public String getDescription() { + return "Find XML namespace URIs, optionally restricting the search by a XPath expression."; + } + + @Override + public TreeVisitor getVisitor() { + XPathMatcher matcher = StringUtils.isBlank(xPath) ? null : new XPathMatcher(xPath); + return new XmlVisitor() { + + @Override + public Xml visitTag(Xml.Tag tag, ExecutionContext ctx) { + Xml.Tag t = (Xml.Tag) super.visitTag(tag, ctx); + if (tag.getNamespaces().containsValue(namespaceUri) && (matcher == null || matcher.matches(getCursor()))) { + t = SearchResult.found(t); + } + return t; + } + }; + } + + public static Set find(Xml x, String namespaceUri, @Nullable String xPath) { + XPathMatcher matcher = StringUtils.isBlank(xPath) ? null : new XPathMatcher(xPath); + Set ts = new HashSet<>(); + new XmlVisitor>() { + @Override + public Xml visitTag(Xml.Tag tag, Set ts) { + if (tag.getNamespaces().containsValue(namespaceUri) && (matcher == null || matcher.matches(getCursor()))) { + ts.add(tag); + } + return super.visitTag(tag, ts); + } + }.visit(x, ts); + return ts; + } +} diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/tree/Xml.java b/rewrite-xml/src/main/java/org/openrewrite/xml/tree/Xml.java index ff3be4ba794..caa74a3e842 100755 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/tree/Xml.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/tree/Xml.java @@ -20,6 +20,8 @@ import org.apache.commons.text.StringEscapeUtils; import org.intellij.lang.annotations.Language; import org.openrewrite.*; +import org.openrewrite.internal.ListUtils; +import org.openrewrite.internal.StringUtils; import org.openrewrite.internal.WhitespaceValidationService; import org.openrewrite.internal.lang.Nullable; import org.openrewrite.marker.Markers; @@ -32,10 +34,7 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Path; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.UUID; +import java.util.*; import java.util.stream.Collectors; import static java.util.Collections.emptyList; @@ -75,20 +74,61 @@ default

boolean isAcceptable(TreeVisitor v, P p) { */ Xml withPrefixUnsafe(String prefix); + static boolean isNamespaceDefinitionAttribute(String name) { + return name.startsWith("xmlns"); + } + + static String getAttributeNameForPrefix(String namespacePrefix) { + return namespacePrefix.isEmpty() ? "xmlns" : "xmlns:" + namespacePrefix; + } + + /** + * Extract the namespace prefix from a namespace definition attribute name (xmlns* attributes). + * + * @param name the attribute name or null if not a namespace definition attribute + * @return the namespace prefix + */ + static @Nullable String extractPrefixFromNamespaceDefinition(String name) { + if (!isNamespaceDefinitionAttribute(name)) { + return null; + } + return "xmlns".equals(name) ? "" : extractLocalName(name); + } + + /** + * Extract the namespace prefix from a tag or attribute name. + * + * @param name the tag or attribute name + * @return the namespace prefix (empty string for the default namespace) + */ + static String extractNamespacePrefix(String name) { + int colon = name.indexOf(':'); + return colon == -1 ? "" : name.substring(0, colon); + } + + /** + * Extract the local name from a tag or attribute name. + * + * @param name the tag or attribute name + * @return the local name + */ + static String extractLocalName(String name) { + int colon = name.indexOf(':'); + return colon == -1 ? name : name.substring(colon + 1); + } + + @Getter @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) @EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true) @RequiredArgsConstructor class Document implements Xml, SourceFile { - @Getter @With @EqualsAndHashCode.Include UUID id; - @Getter @With Path sourcePath; - @Getter @With String prefixUnsafe; @@ -102,26 +142,21 @@ public String getPrefix() { return prefixUnsafe; } - @Getter @With Markers markers; - @Getter @Nullable // for backwards compatibility @With(AccessLevel.PRIVATE) String charsetName; @With - @Getter boolean charsetBomMarked; @With - @Getter @Nullable Checksum checksum; @With - @Getter @Nullable FileAttributes fileAttributes; @@ -130,20 +165,24 @@ public Charset getCharset() { return charsetName == null ? StandardCharsets.UTF_8 : Charset.forName(charsetName); } + @SuppressWarnings("unchecked") @Override - public SourceFile withCharset(Charset charset) { + public Xml.Document withCharset(Charset charset) { return withCharsetName(charset.name()); } - @Getter @With Prolog prolog; - @Getter - @With Tag root; - @Getter + public Document withRoot(Tag root) { + if (this.root == root) { + return this; + } + return new Document(id, sourcePath, prefixUnsafe, markers, charsetName, charsetBomMarked, checksum, fileAttributes, prolog, root, eof); + } + String eof; public Document withEof(String eof) { @@ -275,6 +314,7 @@ public

Xml acceptXml(XmlVisitor

v, P p) { } } + @SuppressWarnings("unused") @Value @EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true) class Tag implements Xml, Content { @@ -285,6 +325,123 @@ class Tag implements Xml, Content { @With String prefixUnsafe; + /** + * The map returned by this method is a view of the Tag's attributes. + * Modifying the map will NOT modify the tag's attributes. + * + * @return a map of namespace prefixes (without the xmlns prefix) to URIs for this tag. + */ + public Map getNamespaces() { + final Map namespaces = new LinkedHashMap<>(attributes.size()); + if (!attributes.isEmpty()) { + for (Attribute attribute : attributes) { + if(isNamespaceDefinitionAttribute(attribute.getKeyAsString())) { + namespaces.put( + extractPrefixFromNamespaceDefinition(attribute.getKeyAsString()), + attribute.getValueAsString()); + } + } + } + return namespaces; + } + + /** + * Gets a map containing all namespaces defined in the current scope, including all parent scopes. + * + * @param cursor the cursor to search from + * @return a map containing all namespaces defined in the current scope, including all parent scopes. + */ + public Map getAllNamespaces(Cursor cursor) { + Map namespaces = getNamespaces(); + while (cursor != null) { + Xml.Tag enclosing = cursor.firstEnclosing(Xml.Tag.class); + if (enclosing != null) { + for (Map.Entry ns : enclosing.getNamespaces().entrySet()) { + if (namespaces.containsValue(ns.getKey())) { + throw new IllegalStateException(java.lang.String.format("Cannot have two namespaces with the same prefix (%s): '%s' and '%s'", ns.getKey(), namespaces.get(ns.getKey()), ns.getValue())); + } + namespaces.put(ns.getKey(), ns.getValue()); + } + } + cursor = cursor.getParent(); + } + + return namespaces; + } + + public Tag withNamespaces(Map namespaces) { + Map currentNamespaces = getNamespaces(); + if (currentNamespaces.equals(namespaces)) { + return this; + } + + List attributes = this.attributes; + if (attributes.isEmpty()) { + for (Map.Entry ns : namespaces.entrySet()) { + String key = getAttributeNameForPrefix(ns.getKey()); + attributes = ListUtils.concat(attributes, new Xml.Attribute( + randomId(), + "", + Markers.EMPTY, + new Xml.Ident( + randomId(), + "", + Markers.EMPTY, + key + ), + "", + new Xml.Attribute.Value( + randomId(), + "", + Markers.EMPTY, + Xml.Attribute.Value.Quote.Double, ns.getValue() + ) + )); + } + } else { + Map attributeByKey = attributes.stream() + .collect(Collectors.toMap( + Attribute::getKeyAsString, + a -> a + )); + + for (Map.Entry ns : namespaces.entrySet()) { + String key = getAttributeNameForPrefix(ns.getKey()); + if (attributeByKey.containsKey(key)) { + Xml.Attribute attribute = attributeByKey.get(key); + if (!ns.getValue().equals(attribute.getValueAsString())) { + ListUtils.map(attributes, a -> a.getKeyAsString().equals(key) + ? attribute.withValue(new Xml.Attribute.Value(randomId(), "", Markers.EMPTY, Xml.Attribute.Value.Quote.Double, ns.getValue())) + : a + ); + } + } else { + attributes = ListUtils.concat(attributes, new Xml.Attribute( + randomId(), + " ", + Markers.EMPTY, + new Xml.Ident( + randomId(), + "", + Markers.EMPTY, + key + ), + "", + new Xml.Attribute.Value( + randomId(), + "", + Markers.EMPTY, + Xml.Attribute.Value.Quote.Double, ns.getValue() + ) + )); + } + } + } + + return new Tag(id, prefixUnsafe, markers, name, attributes, content, closing, + beforeTagDelimiterPrefix); + } + @Override public Tag withPrefix(String prefix) { return WithPrefix.onlyIfNotEqual(this, prefix); @@ -461,6 +618,29 @@ public Tag withContent(@Nullable List content) { @With String beforeTagDelimiterPrefix; + /** + * @return The local name for this tag, without any namespace prefix. + */ + public String getLocalName() { + return extractLocalName(name); + } + + /** + * @return The namespace prefix for this tag, if any. + */ + public Optional getNamespacePrefix() { + String extractedNamespacePrefix = extractNamespacePrefix(name); + return Optional.ofNullable(StringUtils.isNotEmpty(extractedNamespacePrefix) ? extractedNamespacePrefix : null); + } + + /** + * @return The namespace URI for this tag, if any. + */ + public Optional getNamespaceUri(Cursor cursor) { + Optional maybeNamespacePrefix = getNamespacePrefix(); + return maybeNamespacePrefix.flatMap(s -> Optional.ofNullable(getAllNamespaces(cursor).get(s))); + } + @Override public

Xml acceptXml(XmlVisitor

v, P p) { return v.visitTag(this, p); diff --git a/rewrite-xml/src/test/java/org/openrewrite/xml/ChangeNamespaceValueTest.java b/rewrite-xml/src/test/java/org/openrewrite/xml/ChangeNamespaceValueTest.java index fc668a0fb8a..38d6e10dfd4 100644 --- a/rewrite-xml/src/test/java/org/openrewrite/xml/ChangeNamespaceValueTest.java +++ b/rewrite-xml/src/test/java/org/openrewrite/xml/ChangeNamespaceValueTest.java @@ -27,7 +27,7 @@ class ChangeNamespaceValueTest implements RewriteTest { @Test void replaceVersion24Test() { rewriteRun( - spec -> spec.recipe(new ChangeNamespaceValue("web-app", null, "http://java.sun.com/xml/ns/j2ee", "2.4", false)) + spec -> spec.recipe(new ChangeNamespaceValue("web-app", null, "http://java.sun.com/xml/ns/j2ee", "2.4", false, null, null)) .expectedCyclesThatMakeChanges(2), xml( """ @@ -53,7 +53,7 @@ void replaceVersion24Test() { @Test void replaceVersion25Test() { rewriteRun( - spec -> spec.recipe(new ChangeNamespaceValue("web-app", null, "http://java.sun.com/xml/ns/java", "2.5,3.0", false)) + spec -> spec.recipe(new ChangeNamespaceValue("web-app", null, "http://java.sun.com/xml/ns/java", "2.5,3.0", false, null, null)) .expectedCyclesThatMakeChanges(2), xml( """ @@ -79,7 +79,7 @@ void replaceVersion25Test() { @Test void replaceVersion30Test() { rewriteRun( - spec -> spec.recipe(new ChangeNamespaceValue("web-app", null, "http://java.sun.com/xml/ns/java", "2.5,3.0", false)) + spec -> spec.recipe(new ChangeNamespaceValue("web-app", null, "http://java.sun.com/xml/ns/java", "2.5,3.0", false, null, null)) .expectedCyclesThatMakeChanges(2), xml( """ @@ -105,7 +105,7 @@ void replaceVersion30Test() { @Test void replaceVersion31Test() { rewriteRun( - spec -> spec.recipe(new ChangeNamespaceValue("web-app", null, "http://xmlns.jcp.org/xml/ns/javaee", "3.1+", false)) + spec -> spec.recipe(new ChangeNamespaceValue("web-app", null, "http://xmlns.jcp.org/xml/ns/javaee", "3.1+", false, null, null)) .expectedCyclesThatMakeChanges(2), xml( """ @@ -131,7 +131,7 @@ void replaceVersion31Test() { @Test void replaceVersion32Test() { rewriteRun( - spec -> spec.recipe(new ChangeNamespaceValue("web-app", null, "http://xmlns.jcp.org/xml/ns/javaee", "3.1+", false)) + spec -> spec.recipe(new ChangeNamespaceValue("web-app", null, "http://xmlns.jcp.org/xml/ns/javaee", "3.1+", false, null, null)) .expectedCyclesThatMakeChanges(2), xml( """ @@ -157,7 +157,7 @@ void replaceVersion32Test() { @Test void invalidVersionTest() { rewriteRun( - spec -> spec.recipe(new ChangeNamespaceValue("web-app", null, "http://java.sun.com/xml/ns/j2ee", "2.5", false)), + spec -> spec.recipe(new ChangeNamespaceValue("web-app", null, "http://java.sun.com/xml/ns/j2ee", "2.5", false, null, null)), xml( """ spec.recipe(new ChangeNamespaceValue(null, "http://old.namespace", "https://new.namespace", null, true)), + spec -> spec.recipe(new ChangeNamespaceValue(null, "http://old.namespace", "https://new.namespace", null, true, null, null)), xml( """ spec.recipe(new ChangeNamespaceValue(null, "http://old.namespace", "https://new.namespace", null, true)), + spec -> spec.recipe(new ChangeNamespaceValue(null, "http://old.namespace", "https://new.namespace", null, true, null, null)), xml( """ spec.recipe(new ChangeNamespaceValue(null, "http://non.existant.namespace", "https://new.namespace", null, true)), + spec -> spec.recipe(new ChangeNamespaceValue(null, "http://non.existant.namespace", "https://new.namespace", null, true, null, null)), xml( """ spec.recipe(new ChangeNamespaceValue("web-app", "http://java.sun.com/xml/ns/j2ee", "http://java.sun.com/xml/ns/javaee", "2.4", true, "2.5", "http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd")), + xml( + """ + + testWebDDNamespace + + """, + """ + + testWebDDNamespace + + """ + ) + ); + } + + @Test + void replaceNamespaceUriAndAddMissingSchemaLocation() { + rewriteRun( + spec -> spec.recipe(new ChangeNamespaceValue("web-app", "http://java.sun.com/xml/ns/j2ee", "http://java.sun.com/xml/ns/javaee", "2.4", true, "2.5", "http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd")), + xml( + """ + + testWebDDNamespace + + """, + """ + + testWebDDNamespace + + """ + ) + ); + } } diff --git a/rewrite-xml/src/test/java/org/openrewrite/xml/CreateXmlFileTest.java b/rewrite-xml/src/test/java/org/openrewrite/xml/CreateXmlFileTest.java index 843b3a66b6f..02e216acd83 100644 --- a/rewrite-xml/src/test/java/org/openrewrite/xml/CreateXmlFileTest.java +++ b/rewrite-xml/src/test/java/org/openrewrite/xml/CreateXmlFileTest.java @@ -15,6 +15,7 @@ */ package org.openrewrite.xml; +import org.intellij.lang.annotations.Language; import org.junit.jupiter.api.Test; import org.openrewrite.DocumentExample; import org.openrewrite.test.RewriteTest; @@ -26,6 +27,7 @@ class CreateXmlFileTest implements RewriteTest { @DocumentExample @Test void hasCreatedFile() { + @Language("xml") String fileContents = """ 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 0836406afad..1fefbf336ae 100755 --- a/rewrite-xml/src/test/java/org/openrewrite/xml/XPathMatcherTest.java +++ b/rewrite-xml/src/test/java/org/openrewrite/xml/XPathMatcherTest.java @@ -159,20 +159,44 @@ void matchPom() { pomXml2)).isTrue(); } + private final SourceFile attributeXml = new XmlParser().parse( + """ + + + baz + + """ + ).toList().get(0); + @Test void attributePredicate() { - SourceFile xml = new XmlParser().parse( - """ - - - baz - - """ - ).toList().get(0); - assertThat(match("/root/element1[@foo='bar']", xml)).isTrue(); - assertThat(match("/root/element1[@foo='baz']", xml)).isFalse(); - assertThat(match("/root/element1[foo='bar']", xml)).isFalse(); - assertThat(match("/root/element1[foo='baz']", xml)).isTrue(); + assertThat(match("/root/element1[@foo='bar']", attributeXml)).isTrue(); + assertThat(match("/root/element1[@foo='baz']", attributeXml)).isFalse(); + assertThat(match("/root/element1[foo='bar']", attributeXml)).isFalse(); + assertThat(match("/root/element1[foo='baz']", attributeXml)).isTrue(); + } + + @Test + void wildcards() { + // condition with wildcard attribute + assertThat(match("/root/element1[@*='bar']", attributeXml)).isTrue(); + assertThat(match("/root/element1[@*='baz']", attributeXml)).isFalse(); + + // condition with wildcard element + assertThat(match("/root/element1[*='bar']", attributeXml)).isFalse(); + assertThat(match("/root/element1[*='baz']", attributeXml)).isTrue(); + + // absolute xpath with wildcard element + assertThat(match("/root/*[@foo='bar']", attributeXml)).isTrue(); + assertThat(match("/root/*[@*='bar']", attributeXml)).isTrue(); + assertThat(match("/root/*[@foo='baz']", attributeXml)).isFalse(); + assertThat(match("/root/*[@*='baz']", attributeXml)).isFalse(); + + // 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='baz']", attributeXml)).isTrue(); } @Test @@ -188,6 +212,8 @@ void relativePathsWithConditions() { """ ).toList().get(0); +// assertThat(match("//element1[foo='bar']", xml)).isFalse(); // TODO: fix - was already failing before * changes + assertThat(match("//element1[foo='baz']", xml)).isTrue(); assertThat(match("//element1[@foo='bar']", xml)).isTrue(); assertThat(match("//element1[foo='baz']/test", xml)).isTrue(); assertThat(match("//element1[foo='baz']/baz", xml)).isFalse(); @@ -204,7 +230,7 @@ void matchFunctions() { // Namespace functions assertThat(match("/*[local-name()='element1']", namespacedXml)).isFalse(); - assertThat(match("//*[local-name()='element1']", namespacedXml)).isFalse(); + assertThat(match("//*[local-name()='element1']", namespacedXml)).isTrue(); assertThat(match("/root/*[local-name()='element1']", namespacedXml)).isTrue(); assertThat(match("/root/*[namespace-uri()='http://www.example.com/namespace2']", namespacedXml)).isTrue(); assertThat(match("/*[namespace-uri()='http://www.example.com/namespace2']", namespacedXml)).isFalse(); @@ -223,6 +249,22 @@ void matchFunctions() { assertThat(match("count(/root/*)", namespacedXml)).isTrue(); } + @Test + void testMatchLocalName() { + assertThat(match("/*[local-name()='root']", namespacedXml)).isTrue(); + assertThat(match("/*[local-name()='element1']", namespacedXml)).isFalse(); + assertThat(match("/*[local-name()='element2']", namespacedXml)).isFalse(); + assertThat(match("//*[local-name()='element1']", namespacedXml)).isTrue(); + assertThat(match("//*[local-name()='element2']", namespacedXml)).isTrue(); + assertThat(match("//*[local-name()='dne']", namespacedXml)).isFalse(); + + assertThat(match("/root[local-name()='root']", namespacedXml)).isTrue(); + assertThat(match("//element1[local-name()='element1']", namespacedXml)).isTrue(); + assertThat(match("//element2[local-name()='element2']", namespacedXml)).isFalse(); + assertThat(match("//ns2:element2[local-name()='element2']", namespacedXml)).isTrue(); + assertThat(match("//dne[local-name()='dne']", namespacedXml)).isFalse(); + } + private boolean match(String xpath, SourceFile x) { XPathMatcher matcher = new XPathMatcher(xpath); return !TreeVisitor.collect(new XmlVisitor<>() { diff --git a/rewrite-xml/src/test/java/org/openrewrite/xml/internal/XmlTest.java b/rewrite-xml/src/test/java/org/openrewrite/xml/internal/XmlTest.java new file mode 100644 index 00000000000..d029151e856 --- /dev/null +++ b/rewrite-xml/src/test/java/org/openrewrite/xml/internal/XmlTest.java @@ -0,0 +1,57 @@ +/* + * 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.internal; + +import org.junit.jupiter.api.Test; +import org.openrewrite.xml.tree.Xml; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class XmlTest { + + @Test + void isNamespaceDefinitionAttributeTests() { + assertThat(Xml.isNamespaceDefinitionAttribute("xmlns:test")).isTrue(); + assertThat(Xml.isNamespaceDefinitionAttribute("test")).isFalse(); + } + + @Test + void getAttributeNameForPrefix() { + assertThat(Xml.getAttributeNameForPrefix("test")).isEqualTo("xmlns:test"); + assertThat(Xml.getAttributeNameForPrefix("")).isEqualTo("xmlns"); + } + + @Test + void extractNamespacePrefix() { + assertEquals("test", Xml.extractNamespacePrefix("test:tag")); + assertEquals("", Xml.extractNamespacePrefix("tag")); + } + + @Test + void extractLocalName() { + assertEquals("tag", Xml.extractLocalName("test:tag")); + assertEquals("tag", Xml.extractLocalName("tag")); + } + + @Test + void extractPrefixFromNamespaceDefinition() { + assertEquals("test", Xml.extractPrefixFromNamespaceDefinition("xmlns:test")); + assertEquals("", Xml.extractPrefixFromNamespaceDefinition("xmlns")); + assertThat(Xml.extractPrefixFromNamespaceDefinition("test")).isEqualTo(null); + assertThat(Xml.extractPrefixFromNamespaceDefinition("a:test")).isEqualTo(null); + } +} diff --git a/rewrite-xml/src/test/java/org/openrewrite/xml/search/FindNamespacePrefixTest.java b/rewrite-xml/src/test/java/org/openrewrite/xml/search/FindNamespacePrefixTest.java new file mode 100644 index 00000000000..2e76cdb0c49 --- /dev/null +++ b/rewrite-xml/src/test/java/org/openrewrite/xml/search/FindNamespacePrefixTest.java @@ -0,0 +1,123 @@ +/* + * Copyright 2020 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.search; + +import org.intellij.lang.annotations.Language; +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.test.RewriteTest; +import org.openrewrite.xml.tree.Xml; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.openrewrite.xml.Assertions.xml; + +class FindNamespacePrefixTest implements RewriteTest { + + @DocumentExample + @Test + void rootElement() { + rewriteRun( + spec -> spec.recipe(new FindNamespacePrefix("xsi", null)), + xml( + source, + """ + + + + + + + + + """ + ) + ); + } + + @Test + void nestedElement() { + rewriteRun( + spec -> spec.recipe(new FindNamespacePrefix("jaxws", null)), + xml( + source, + """ + + + + + + + + + """ + ) + ); + } + + @Test + void noMatchOnNamespacePrefix() { + rewriteRun( + spec -> spec.recipe(new FindNamespacePrefix("foo", null)), + xml(source) + ); + } + + @Test + void noMatchOnXPath() { + rewriteRun( + spec -> spec.recipe(new FindNamespacePrefix("xsi", "/jaxws:client")), + xml(source) + ); + } + + @Test + void staticFind() { + rewriteRun( + xml( + source, + spec -> spec.beforeRecipe(xml -> assertThat(FindNamespacePrefix.find(xml, "xsi", null)) + .isNotEmpty() + .hasSize(1) + .hasOnlyElementsOfType(Xml.Tag.class) + ) + ) + ); + } + + @Language("xml") + private final String source = """ + + + + + + + + + """; +} diff --git a/rewrite-xml/src/test/java/org/openrewrite/xml/search/HasNamespaceUriTest.java b/rewrite-xml/src/test/java/org/openrewrite/xml/search/HasNamespaceUriTest.java new file mode 100644 index 00000000000..fe788ae0465 --- /dev/null +++ b/rewrite-xml/src/test/java/org/openrewrite/xml/search/HasNamespaceUriTest.java @@ -0,0 +1,123 @@ +/* + * Copyright 2020 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.search; + +import org.intellij.lang.annotations.Language; +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.test.RewriteTest; +import org.openrewrite.xml.tree.Xml; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.openrewrite.xml.Assertions.xml; + +class HasNamespaceUriTest implements RewriteTest { + + @DocumentExample + @Test + void rootElement() { + rewriteRun( + spec -> spec.recipe(new HasNamespaceUri("http://www.w3.org/2001/XMLSchema-instance", null)), + xml( + source, + """ + + + + + + + + + """ + ) + ); + } + + @Test + void nestedElement() { + rewriteRun( + spec -> spec.recipe(new HasNamespaceUri("http://cxf.apache.org/jaxws", null)), + xml( + source, + """ + + + + + + + + + """ + ) + ); + } + + @Test + void noMatchOnNamespaceUri() { + rewriteRun( + spec -> spec.recipe(new HasNamespaceUri("foo", null)), + xml(source) + ); + } + + @Test + void noMatchOnXPath() { + rewriteRun( + spec -> spec.recipe(new HasNamespaceUri("xsi", "/jaxws:client")), + xml(source) + ); + } + + @Test + void staticFind() { + rewriteRun( + xml( + source, + spec -> spec.beforeRecipe(xml -> assertThat(HasNamespaceUri.find(xml, "http://www.w3.org/2001/XMLSchema-instance", null)) + .isNotEmpty() + .hasSize(1) + .hasOnlyElementsOfType(Xml.Tag.class) + ) + ) + ); + } + + @Language("xml") + private final String source = """ + + + + + + + + + """; +}