From 512d05eb568437d18c434e8fa505cb04f766db08 Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Tue, 16 Jan 2024 11:08:48 -0500 Subject: [PATCH 01/21] Partial support for parsing XML namespaces --- .../xml/internal/XmlParserVisitor.java | 39 ++++++++++++------- .../openrewrite/xml/internal/XmlUtils.java | 34 ++++++++++++++++ .../java/org/openrewrite/xml/tree/Xml.java | 27 +++++++++++-- .../openrewrite/xml/CreateXmlFileTest.java | 2 + 4 files changed, 85 insertions(+), 17 deletions(-) create mode 100644 rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlUtils.java diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlParserVisitor.java b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlParserVisitor.java index 5c6dc9967dd..dcea24aab08 100755 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlParserVisitor.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlParserVisitor.java @@ -33,6 +33,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.function.BiFunction; import static java.util.stream.Collectors.toList; @@ -60,18 +61,28 @@ public XmlParserVisitor(Path path, @Nullable FileAttributes fileAttributes, Stri @Override public Xml.Document visitDocument(XMLParser.DocumentContext ctx) { - return convert(ctx, (c, prefix) -> new Xml.Document( - randomId(), - path, - prefix, - Markers.EMPTY, - charset.name(), - charsetBomMarked, - null, - fileAttributes, - visitProlog(ctx.prolog()), - visitElement(ctx.element()), - source.substring(cursor)) + return convert(ctx, (c, prefix) -> { + Xml.Prolog prolog = visitProlog(ctx.prolog()); + Xml.Tag root = visitElement(ctx.element()); + + Map namespaces = root != null + ? XmlUtils.extractNamespaces(root.getAttributes()) + : Collections.emptyMap(); + + return new Xml.Document( + randomId(), + path, + prefix, + namespaces, + Markers.EMPTY, + charset.name(), + charsetBomMarked, + null, + fileAttributes, + prolog, + root, + source.substring(cursor)); + } ); } @@ -229,6 +240,8 @@ public Xml.Tag visitElement(XMLParser.ElementContext ctx) { List attributes = ctx.attribute().stream().map(this::visitAttribute).collect(toList()); + Map namespaces = XmlUtils.extractNamespaces(attributes); + List content = null; String beforeTagDelimiterPrefix; Xml.Tag.Closing closeTag = null; @@ -258,7 +271,7 @@ public Xml.Tag visitElement(XMLParser.ElementContext ctx) { cursor++; } - return new Xml.Tag(randomId(), prefix, Markers.EMPTY, name, attributes, + return new Xml.Tag(randomId(), prefix, namespaces, Markers.EMPTY, name, attributes, content, closeTag, beforeTagDelimiterPrefix); } ); diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlUtils.java b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlUtils.java new file mode 100644 index 00000000000..791e514ccee --- /dev/null +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlUtils.java @@ -0,0 +1,34 @@ +package org.openrewrite.xml.internal; + +import org.openrewrite.xml.tree.Xml; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Collectors; + +public class XmlUtils { + + private XmlUtils() { + } + + private static boolean isNamespaceAttribute(String name) { + return name.startsWith("xmlns"); + } + + private static String extractNamespacePrefix(String name) { + int colon = name.indexOf(':'); + return colon == -1 ? "" : name.substring(colon + 1); + } + + public static Map extractNamespaces(Collection attributes) { + return attributes.isEmpty() + ? Collections.emptyMap() + : attributes.stream() + .filter(attribute -> isNamespaceAttribute(attribute.getKeyAsString())) + .collect(Collectors.toMap( + attribute -> extractNamespacePrefix(attribute.getKeyAsString()), + attribute -> attribute.getValue().getValue() + )); + } +} 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 9dbd8869b1c..e09ea4042cd 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 @@ -26,12 +26,14 @@ import org.openrewrite.xml.XmlVisitor; import org.openrewrite.xml.internal.WithPrefix; import org.openrewrite.xml.internal.XmlPrinter; +import org.openrewrite.xml.internal.XmlUtils; 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.Map; import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; @@ -89,6 +91,9 @@ class Document implements Xml, SourceFile { @With String prefixUnsafe; + @With + Map namespaces; + public Document withPrefix(String prefix) { return WithPrefix.onlyIfNotEqual(this, prefix); } @@ -134,9 +139,16 @@ public SourceFile withCharset(Charset charset) { Prolog prolog; @Getter - @With Tag root; + public Document withRoot(Tag root) { + if (this.root == root) { + return this; + } + Map namespaces = XmlUtils.extractNamespaces(root.getAttributes()); + return new Document(id, sourcePath, prefixUnsafe, namespaces, markers, charsetName, charsetBomMarked, checksum, fileAttributes, prolog, root, eof); + } + @Getter String eof; @@ -144,7 +156,7 @@ public Document withEof(String eof) { if (this.eof.equals(eof)) { return this; } - return new Document(id, sourcePath, prefixUnsafe, markers, charsetName, charsetBomMarked, checksum, fileAttributes, prolog, root, eof); + return new Document(id, sourcePath, prefixUnsafe, namespaces, markers, charsetName, charsetBomMarked, checksum, fileAttributes, prolog, root, eof); } @Override @@ -261,6 +273,13 @@ class Tag implements Xml, Content { @With String prefixUnsafe; + Map namespaces; + + public Tag withNamespaces(Map namespaces) { + return new Tag(id, prefixUnsafe, namespaces, markers, name, attributes, content, closing, + beforeTagDelimiterPrefix); + } + public Tag withPrefix(String prefix) { return WithPrefix.onlyIfNotEqual(this, prefix); } @@ -286,7 +305,7 @@ public static Xml.Tag build(@Language("xml") String tagSource) { } public Tag withName(String name) { - return new Tag(id, prefixUnsafe, markers, name, attributes, content, + return new Tag(id, prefixUnsafe, namespaces, markers, name, attributes, content, closing == null ? null : closing.withName(name), beforeTagDelimiterPrefix); } @@ -394,7 +413,7 @@ public Tag withContent(@Nullable List content) { return this; } - Tag tag = new Tag(id, prefixUnsafe, markers, name, attributes, content, closing, + Tag tag = new Tag(id, prefixUnsafe, namespaces, markers, name, attributes, content, closing, beforeTagDelimiterPrefix); if (closing == null) { 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 17b30e0c9f9..d39d8efdde5 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 = """ From 7b65a6810d39ebc3f819fc817a85323312f29ac0 Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Tue, 16 Jan 2024 11:40:36 -0500 Subject: [PATCH 02/21] Adding namespace resolution --- .../openrewrite/xml/internal/XmlUtils.java | 22 +++++++++- .../java/org/openrewrite/xml/tree/Xml.java | 41 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlUtils.java b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlUtils.java index 791e514ccee..54c709b617f 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlUtils.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlUtils.java @@ -1,10 +1,13 @@ package org.openrewrite.xml.internal; +import org.openrewrite.Cursor; +import org.openrewrite.internal.lang.NonNull; import org.openrewrite.xml.tree.Xml; import java.util.Collection; import java.util.Collections; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; public class XmlUtils { @@ -16,11 +19,13 @@ private static boolean isNamespaceAttribute(String name) { return name.startsWith("xmlns"); } - private static String extractNamespacePrefix(String name) { + @NonNull + public static String extractNamespacePrefix(String name) { int colon = name.indexOf(':'); return colon == -1 ? "" : name.substring(colon + 1); } + @NonNull public static Map extractNamespaces(Collection attributes) { return attributes.isEmpty() ? Collections.emptyMap() @@ -31,4 +36,19 @@ public static Map extractNamespaces(Collection at attribute -> attribute.getValue().getValue() )); } + + @NonNull + public static Optional findNamespacePrefix(Cursor cursor, String namespacePrefix) { + String resolvedNamespace = null; + while (cursor != null) { + Xml.Tag enclosing = cursor.firstEnclosing(Xml.Tag.class); + if (enclosing != null) { + resolvedNamespace = enclosing.getNamespaces().get(namespacePrefix); + break; + } + cursor = cursor.getParent(); + } + + return Optional.ofNullable(resolvedNamespace); + } } 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 e09ea4042cd..d2f444a35fe 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 @@ -448,6 +448,31 @@ public Tag withContent(@Nullable List content) { @With String beforeTagDelimiterPrefix; + /** + * @return The namespace prefix for this tag, if any. + */ + public Optional getNamespacePrefix() { + String namespacePrefix = XmlUtils.extractNamespacePrefix(name); + return Optional.ofNullable(namespacePrefix.isEmpty() ? null : namespacePrefix); + } + + /** + * @return The namespace URI for this tag, if any. + */ + public Optional getNamespaceUri(Cursor cursor) { + Optional maybeNamespacePrefix = getNamespacePrefix(); + if (!maybeNamespacePrefix.isPresent()) { + return Optional.empty(); + } + + String namespacePrefix = maybeNamespacePrefix.get(); + if (namespaces != null && namespaces.containsKey(namespacePrefix)) { + return Optional.of(namespaces.get(namespacePrefix)); + } + + return XmlUtils.findNamespacePrefix(cursor, namespacePrefix); + } + @Override public

Xml acceptXml(XmlVisitor

v, P p) { return v.visitTag(this, p); @@ -513,6 +538,22 @@ public String getPrefix() { String beforeEquals; Value value; + /** + * @return The namespace prefix for this attribute, if any. + */ + public Optional getNamespacePrefix() { + String namespacePrefix = XmlUtils.extractNamespacePrefix(key.getName()); + return Optional.ofNullable(namespacePrefix.isEmpty() ? null : namespacePrefix); + } + + /** + * @return The namespace URI for this attribute, if any. + */ + public Optional getNamespaceUri(Cursor cursor) { + Optional maybeNamespacePrefix = getNamespacePrefix(); + return maybeNamespacePrefix.flatMap(s -> XmlUtils.findNamespacePrefix(cursor, s)); + } + @Override public

Xml acceptXml(XmlVisitor

v, P p) { return v.visitAttribute(this, p); From 9d4c92452e39273a8ed5af46a7920215779f9eea Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Tue, 16 Jan 2024 11:45:02 -0500 Subject: [PATCH 03/21] Missing license header --- .../org/openrewrite/xml/internal/XmlUtils.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlUtils.java b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlUtils.java index 54c709b617f..21d51bbdf6d 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlUtils.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlUtils.java @@ -1,3 +1,18 @@ +/* + * 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.openrewrite.Cursor; From 520a282ef9278b88e47a55500e032ee36162313b Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Tue, 16 Jan 2024 12:25:30 -0500 Subject: [PATCH 04/21] Adding recipes to search namespace URIs/prefixes --- .../xml/search/HasNamespacePrefix.java | 90 +++++++++++++ .../xml/search/HasNamespaceUri.java | 90 +++++++++++++ .../xml/search/HasNamespacePrefixTest.java | 123 ++++++++++++++++++ .../xml/search/HasNamespaceUriTest.java | 123 ++++++++++++++++++ 4 files changed, 426 insertions(+) create mode 100644 rewrite-xml/src/main/java/org/openrewrite/xml/search/HasNamespacePrefix.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/search/HasNamespacePrefixTest.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/search/HasNamespacePrefix.java b/rewrite-xml/src/main/java/org/openrewrite/xml/search/HasNamespacePrefix.java new file mode 100644 index 00000000000..c0721d80669 --- /dev/null +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/search/HasNamespacePrefix.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 = true) +public class HasNamespacePrefix 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..dda5ade8e7e --- /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 = true) +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/test/java/org/openrewrite/xml/search/HasNamespacePrefixTest.java b/rewrite-xml/src/test/java/org/openrewrite/xml/search/HasNamespacePrefixTest.java new file mode 100644 index 00000000000..4ebe87a9bbb --- /dev/null +++ b/rewrite-xml/src/test/java/org/openrewrite/xml/search/HasNamespacePrefixTest.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 HasNamespacePrefixTest implements RewriteTest { + + @DocumentExample + @Test + void rootElement() { + rewriteRun( + spec -> spec.recipe(new HasNamespacePrefix("xsi", null)), + xml( + source, + """ + + + + + + + + + """ + ) + ); + } + + @Test + void nestedElement() { + rewriteRun( + spec -> spec.recipe(new HasNamespacePrefix("jaxws", null)), + xml( + source, + """ + + + + + + + + + """ + ) + ); + } + + @Test + void noMatchOnNamespacePrefix() { + rewriteRun( + spec -> spec.recipe(new HasNamespacePrefix("foo", null)), + xml(source) + ); + } + + @Test + void noMatchOnXPath() { + rewriteRun( + spec -> spec.recipe(new HasNamespacePrefix("xsi", "/jaxws:client")), + xml(source) + ); + } + + @Test + void staticFind() { + rewriteRun( + xml( + source, + spec -> spec.beforeRecipe(xml -> assertThat(HasNamespacePrefix.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 = """ + + + + + + + + + """; +} From 1ec44eb58b73b9942288991d6e49ba5f8fb4e0a8 Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Tue, 16 Jan 2024 12:51:02 -0500 Subject: [PATCH 05/21] Namespace shortcut methods on \'Xml.Document\' --- .../main/java/org/openrewrite/xml/tree/Xml.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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 d2f444a35fe..fe075ce0559 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 @@ -159,6 +159,20 @@ public Document withEof(String eof) { return new Document(id, sourcePath, prefixUnsafe, namespaces, markers, charsetName, charsetBomMarked, checksum, fileAttributes, prolog, root, eof); } + /** + * @return The namespace prefix of the root tag of this document, if any. + */ + public Optional getNamespacePrefix() { + return root == null ? Optional.empty() : root.getNamespacePrefix(); + } + + /** + * @return The namespace URI of the root tag of this document, if any. + */ + public Optional getNamespaceUri(Cursor cursor) { + return root == null ? Optional.empty() : root.getNamespaceUri(cursor); + } + @Override public

Xml acceptXml(XmlVisitor

v, P p) { return v.visitDocument(this, p); From f5a362199b9f585c551c0993709a5023b1e4ff1f Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Tue, 16 Jan 2024 13:24:30 -0500 Subject: [PATCH 06/21] Change implementation to rely only on attributes --- .../xml/internal/XmlParserVisitor.java | 39 +++----- .../java/org/openrewrite/xml/tree/Xml.java | 98 +++++++++++++++++-- 2 files changed, 102 insertions(+), 35 deletions(-) diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlParserVisitor.java b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlParserVisitor.java index dcea24aab08..5c6dc9967dd 100755 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlParserVisitor.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlParserVisitor.java @@ -33,7 +33,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.function.BiFunction; import static java.util.stream.Collectors.toList; @@ -61,28 +60,18 @@ public XmlParserVisitor(Path path, @Nullable FileAttributes fileAttributes, Stri @Override public Xml.Document visitDocument(XMLParser.DocumentContext ctx) { - return convert(ctx, (c, prefix) -> { - Xml.Prolog prolog = visitProlog(ctx.prolog()); - Xml.Tag root = visitElement(ctx.element()); - - Map namespaces = root != null - ? XmlUtils.extractNamespaces(root.getAttributes()) - : Collections.emptyMap(); - - return new Xml.Document( - randomId(), - path, - prefix, - namespaces, - Markers.EMPTY, - charset.name(), - charsetBomMarked, - null, - fileAttributes, - prolog, - root, - source.substring(cursor)); - } + return convert(ctx, (c, prefix) -> new Xml.Document( + randomId(), + path, + prefix, + Markers.EMPTY, + charset.name(), + charsetBomMarked, + null, + fileAttributes, + visitProlog(ctx.prolog()), + visitElement(ctx.element()), + source.substring(cursor)) ); } @@ -240,8 +229,6 @@ public Xml.Tag visitElement(XMLParser.ElementContext ctx) { List attributes = ctx.attribute().stream().map(this::visitAttribute).collect(toList()); - Map namespaces = XmlUtils.extractNamespaces(attributes); - List content = null; String beforeTagDelimiterPrefix; Xml.Tag.Closing closeTag = null; @@ -271,7 +258,7 @@ public Xml.Tag visitElement(XMLParser.ElementContext ctx) { cursor++; } - return new Xml.Tag(randomId(), prefix, namespaces, Markers.EMPTY, name, attributes, + return new Xml.Tag(randomId(), prefix, Markers.EMPTY, name, attributes, content, closeTag, beforeTagDelimiterPrefix); } ); 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 fe075ce0559..76d56190a5b 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,7 @@ import org.apache.commons.text.StringEscapeUtils; import org.intellij.lang.annotations.Language; import org.openrewrite.*; +import org.openrewrite.internal.ListUtils; import org.openrewrite.internal.lang.Nullable; import org.openrewrite.marker.Markers; import org.openrewrite.xml.XmlParser; @@ -91,8 +92,21 @@ class Document implements Xml, SourceFile { @With String prefixUnsafe; - @With - Map namespaces; + public Map getNamespaces() { + if (root == null) { + throw new IllegalStateException("Cannot get namespaces if root tag is null"); + } + + return root.getNamespaces(); + } + + public Document withNamespaces(Map namespaces) { + if (root == null) { + throw new IllegalStateException("Cannot add namespaces if root tag is null"); + } + + return withRoot(root.withNamespaces(namespaces)); + } public Document withPrefix(String prefix) { return WithPrefix.onlyIfNotEqual(this, prefix); @@ -146,7 +160,7 @@ public Document withRoot(Tag root) { return this; } Map namespaces = XmlUtils.extractNamespaces(root.getAttributes()); - return new Document(id, sourcePath, prefixUnsafe, namespaces, markers, charsetName, charsetBomMarked, checksum, fileAttributes, prolog, root, eof); + return new Document(id, sourcePath, prefixUnsafe, markers, charsetName, charsetBomMarked, checksum, fileAttributes, prolog, root, eof); } @Getter @@ -156,7 +170,7 @@ public Document withEof(String eof) { if (this.eof.equals(eof)) { return this; } - return new Document(id, sourcePath, prefixUnsafe, namespaces, markers, charsetName, charsetBomMarked, checksum, fileAttributes, prolog, root, eof); + return new Document(id, sourcePath, prefixUnsafe, markers, charsetName, charsetBomMarked, checksum, fileAttributes, prolog, root, eof); } /** @@ -287,10 +301,75 @@ class Tag implements Xml, Content { @With String prefixUnsafe; - Map namespaces; + public Map getNamespaces() { + return XmlUtils.extractNamespaces(attributes); + } public Tag withNamespaces(Map namespaces) { - return new Tag(id, prefixUnsafe, namespaces, markers, name, attributes, content, closing, + List attributes = this.attributes; + if (attributes.isEmpty()) { + for (Map.Entry ns : namespaces.entrySet()) { + String key = ns.getKey().isEmpty() ? "xmlns" : "xmlns:" + 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 = ns.getKey().isEmpty() ? "xmlns" : "xmlns:" + 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); } @@ -319,7 +398,7 @@ public static Xml.Tag build(@Language("xml") String tagSource) { } public Tag withName(String name) { - return new Tag(id, prefixUnsafe, namespaces, markers, name, attributes, content, + return new Tag(id, prefixUnsafe, markers, name, attributes, content, closing == null ? null : closing.withName(name), beforeTagDelimiterPrefix); } @@ -427,7 +506,7 @@ public Tag withContent(@Nullable List content) { return this; } - Tag tag = new Tag(id, prefixUnsafe, namespaces, markers, name, attributes, content, closing, + Tag tag = new Tag(id, prefixUnsafe, markers, name, attributes, content, closing, beforeTagDelimiterPrefix); if (closing == null) { @@ -480,7 +559,8 @@ public Optional getNamespaceUri(Cursor cursor) { } String namespacePrefix = maybeNamespacePrefix.get(); - if (namespaces != null && namespaces.containsKey(namespacePrefix)) { + Map namespaces = XmlUtils.extractNamespaces(attributes); + if (namespaces.containsKey(namespacePrefix)) { return Optional.of(namespaces.get(namespacePrefix)); } From aecc3e6c3b9711ce0a85efcf76b2cec663c6297d Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Tue, 16 Jan 2024 13:35:36 -0500 Subject: [PATCH 07/21] Javadocs and cleanup --- .../java/org/openrewrite/xml/internal/XmlUtils.java | 8 ++++++++ .../src/main/java/org/openrewrite/xml/tree/Xml.java | 10 ++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlUtils.java b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlUtils.java index 21d51bbdf6d..610050d0897 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlUtils.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlUtils.java @@ -34,8 +34,16 @@ private static boolean isNamespaceAttribute(String name) { return name.startsWith("xmlns"); } + @NonNull + public static String getAttributeNameFor(String namespacePrefix) { + return namespacePrefix.isEmpty() ? "xmlns" : "xmlns:" + namespacePrefix; + } + @NonNull public static String extractNamespacePrefix(String name) { + if (!isNamespaceAttribute("xmlns")) { + throw new IllegalArgumentException("Namespace attribute names must start with \"xmlns\"."); + } int colon = name.indexOf(':'); return colon == -1 ? "" : name.substring(colon + 1); } 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 76d56190a5b..2421f605475 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 @@ -92,6 +92,9 @@ class Document implements Xml, SourceFile { @With String prefixUnsafe; + /** + * @return a map of namespace prefixes (without the xmlns prefix) to URIs for this document. + */ public Map getNamespaces() { if (root == null) { throw new IllegalStateException("Cannot get namespaces if root tag is null"); @@ -301,6 +304,9 @@ class Tag implements Xml, Content { @With String prefixUnsafe; + /** + * @return a map of namespace prefixes (without the xmlns prefix) to URIs for this tag. + */ public Map getNamespaces() { return XmlUtils.extractNamespaces(attributes); } @@ -309,7 +315,7 @@ public Tag withNamespaces(Map namespaces) { List attributes = this.attributes; if (attributes.isEmpty()) { for (Map.Entry ns : namespaces.entrySet()) { - String key = ns.getKey().isEmpty() ? "xmlns" : "xmlns:" + ns.getKey(); + String key = XmlUtils.getAttributeNameFor(ns.getKey()); attributes = ListUtils.concat(attributes, new Xml.Attribute( randomId(), "", @@ -337,7 +343,7 @@ public Tag withNamespaces(Map namespaces) { )); for (Map.Entry ns : namespaces.entrySet()) { - String key = ns.getKey().isEmpty() ? "xmlns" : "xmlns:" + ns.getKey(); + String key = XmlUtils.getAttributeNameFor(ns.getKey()); if (attributeByKey.containsKey(key)) { Xml.Attribute attribute = attributeByKey.get(key); if (!ns.getValue().equals(attribute.getValueAsString())) { From 385cb7b3a726bb3e3b6ac273c3088c325a428b95 Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Tue, 16 Jan 2024 20:54:10 +0100 Subject: [PATCH 08/21] Rename XmlNamespaceUtils & minor polish Remove duplicate NonNull; see package-info.java Validate argument not literal Apply formatter --- .../{XmlUtils.java => XmlNamespaceUtils.java} | 21 +++++++------------ .../java/org/openrewrite/xml/tree/Xml.java | 20 +++++++++--------- 2 files changed, 18 insertions(+), 23 deletions(-) rename rewrite-xml/src/main/java/org/openrewrite/xml/internal/{XmlUtils.java => XmlNamespaceUtils.java} (80%) diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlUtils.java b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlNamespaceUtils.java similarity index 80% rename from rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlUtils.java rename to rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlNamespaceUtils.java index 610050d0897..7e74fcc052d 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlUtils.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlNamespaceUtils.java @@ -16,7 +16,6 @@ package org.openrewrite.xml.internal; import org.openrewrite.Cursor; -import org.openrewrite.internal.lang.NonNull; import org.openrewrite.xml.tree.Xml; import java.util.Collection; @@ -25,42 +24,38 @@ import java.util.Optional; import java.util.stream.Collectors; -public class XmlUtils { +public class XmlNamespaceUtils { - private XmlUtils() { + private XmlNamespaceUtils() { } private static boolean isNamespaceAttribute(String name) { return name.startsWith("xmlns"); } - @NonNull public static String getAttributeNameFor(String namespacePrefix) { return namespacePrefix.isEmpty() ? "xmlns" : "xmlns:" + namespacePrefix; } - @NonNull public static String extractNamespacePrefix(String name) { - if (!isNamespaceAttribute("xmlns")) { + if (!isNamespaceAttribute(name)) { throw new IllegalArgumentException("Namespace attribute names must start with \"xmlns\"."); } int colon = name.indexOf(':'); return colon == -1 ? "" : name.substring(colon + 1); } - @NonNull public static Map extractNamespaces(Collection attributes) { return attributes.isEmpty() ? Collections.emptyMap() : attributes.stream() - .filter(attribute -> isNamespaceAttribute(attribute.getKeyAsString())) - .collect(Collectors.toMap( - attribute -> extractNamespacePrefix(attribute.getKeyAsString()), - attribute -> attribute.getValue().getValue() - )); + .filter(attribute -> isNamespaceAttribute(attribute.getKeyAsString())) + .collect(Collectors.toMap( + attribute -> extractNamespacePrefix(attribute.getKeyAsString()), + attribute -> attribute.getValue().getValue() + )); } - @NonNull public static Optional findNamespacePrefix(Cursor cursor, String namespacePrefix) { String resolvedNamespace = null; while (cursor != null) { 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 2421f605475..3d34a80a9a3 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 @@ -26,8 +26,8 @@ import org.openrewrite.xml.XmlParser; import org.openrewrite.xml.XmlVisitor; import org.openrewrite.xml.internal.WithPrefix; +import org.openrewrite.xml.internal.XmlNamespaceUtils; import org.openrewrite.xml.internal.XmlPrinter; -import org.openrewrite.xml.internal.XmlUtils; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -162,7 +162,7 @@ public Document withRoot(Tag root) { if (this.root == root) { return this; } - Map namespaces = XmlUtils.extractNamespaces(root.getAttributes()); + Map namespaces = XmlNamespaceUtils.extractNamespaces(root.getAttributes()); return new Document(id, sourcePath, prefixUnsafe, markers, charsetName, charsetBomMarked, checksum, fileAttributes, prolog, root, eof); } @@ -308,14 +308,14 @@ class Tag implements Xml, Content { * @return a map of namespace prefixes (without the xmlns prefix) to URIs for this tag. */ public Map getNamespaces() { - return XmlUtils.extractNamespaces(attributes); + return XmlNamespaceUtils.extractNamespaces(attributes); } public Tag withNamespaces(Map namespaces) { List attributes = this.attributes; if (attributes.isEmpty()) { for (Map.Entry ns : namespaces.entrySet()) { - String key = XmlUtils.getAttributeNameFor(ns.getKey()); + String key = XmlNamespaceUtils.getAttributeNameFor(ns.getKey()); attributes = ListUtils.concat(attributes, new Xml.Attribute( randomId(), "", @@ -343,7 +343,7 @@ public Tag withNamespaces(Map namespaces) { )); for (Map.Entry ns : namespaces.entrySet()) { - String key = XmlUtils.getAttributeNameFor(ns.getKey()); + String key = XmlNamespaceUtils.getAttributeNameFor(ns.getKey()); if (attributeByKey.containsKey(key)) { Xml.Attribute attribute = attributeByKey.get(key); if (!ns.getValue().equals(attribute.getValueAsString())) { @@ -551,7 +551,7 @@ public Tag withContent(@Nullable List content) { * @return The namespace prefix for this tag, if any. */ public Optional getNamespacePrefix() { - String namespacePrefix = XmlUtils.extractNamespacePrefix(name); + String namespacePrefix = XmlNamespaceUtils.extractNamespacePrefix(name); return Optional.ofNullable(namespacePrefix.isEmpty() ? null : namespacePrefix); } @@ -565,12 +565,12 @@ public Optional getNamespaceUri(Cursor cursor) { } String namespacePrefix = maybeNamespacePrefix.get(); - Map namespaces = XmlUtils.extractNamespaces(attributes); + Map namespaces = XmlNamespaceUtils.extractNamespaces(attributes); if (namespaces.containsKey(namespacePrefix)) { return Optional.of(namespaces.get(namespacePrefix)); } - return XmlUtils.findNamespacePrefix(cursor, namespacePrefix); + return XmlNamespaceUtils.findNamespacePrefix(cursor, namespacePrefix); } @Override @@ -642,7 +642,7 @@ public String getPrefix() { * @return The namespace prefix for this attribute, if any. */ public Optional getNamespacePrefix() { - String namespacePrefix = XmlUtils.extractNamespacePrefix(key.getName()); + String namespacePrefix = XmlNamespaceUtils.extractNamespacePrefix(key.getName()); return Optional.ofNullable(namespacePrefix.isEmpty() ? null : namespacePrefix); } @@ -651,7 +651,7 @@ public Optional getNamespacePrefix() { */ public Optional getNamespaceUri(Cursor cursor) { Optional maybeNamespacePrefix = getNamespacePrefix(); - return maybeNamespacePrefix.flatMap(s -> XmlUtils.findNamespacePrefix(cursor, s)); + return maybeNamespacePrefix.flatMap(s -> XmlNamespaceUtils.findNamespacePrefix(cursor, s)); } @Override From c88e2ae016f934b84a0c5a59fab72cf52148bc86 Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Tue, 16 Jan 2024 19:44:06 -0500 Subject: [PATCH 09/21] Fix namespace search on XML hierarchy --- .../xml/internal/XmlNamespaceUtils.java | 118 +++++++++++++++--- .../java/org/openrewrite/xml/tree/Xml.java | 19 +-- .../xml/internal/XmlNamespaceUtilsTest.java | 55 ++++++++ 3 files changed, 166 insertions(+), 26 deletions(-) create mode 100644 rewrite-xml/src/test/java/org/openrewrite/xml/internal/XmlNamespaceUtilsTest.java diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlNamespaceUtils.java b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlNamespaceUtils.java index 7e74fcc052d..4495fb45b0e 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlNamespaceUtils.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlNamespaceUtils.java @@ -15,58 +15,142 @@ */ package org.openrewrite.xml.internal; +import lombok.Value; import org.openrewrite.Cursor; +import org.openrewrite.internal.lang.Nullable; import org.openrewrite.xml.tree.Xml; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.Map; -import java.util.Optional; import java.util.stream.Collectors; public class XmlNamespaceUtils { + public static final String XML_SCHEMA_INSTANCE_PREFIX = "xsi"; + public static final String XML_SCHEMA_INSTANCE_URI = "http://www.w3.org/2001/XMLSchema-instance"; + private XmlNamespaceUtils() { } - private static boolean isNamespaceAttribute(String name) { + public static boolean isNamespaceDefinitionAttribute(String name) { return name.startsWith("xmlns"); } - public static String getAttributeNameFor(String namespacePrefix) { + public static String getAttributeNameForPrefix(String namespacePrefix) { return namespacePrefix.isEmpty() ? "xmlns" : "xmlns:" + namespacePrefix; } + /** + * 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) + */ public static String extractNamespacePrefix(String name) { - if (!isNamespaceAttribute(name)) { - throw new IllegalArgumentException("Namespace attribute names must start with \"xmlns\"."); - } int colon = name.indexOf(':'); - return colon == -1 ? "" : name.substring(colon + 1); + 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); + } + + /** + * Extract the namespace prefix from a namespace definition attribute name (xmlns* attributes). + * + * @param name the attribute name + * @return the namespace prefix + */ + public static String extractPrefixFromNamespaceDefinition(String name) { + if (!isNamespaceDefinitionAttribute(name)) { + throw new IllegalArgumentException("Namespace definition attribute names must start with \"xmlns\"."); + } + return "xmlns".equals(name) ? "" : extractLocalName(name); } public static Map extractNamespaces(Collection attributes) { return attributes.isEmpty() ? Collections.emptyMap() : attributes.stream() - .filter(attribute -> isNamespaceAttribute(attribute.getKeyAsString())) - .collect(Collectors.toMap( - attribute -> extractNamespacePrefix(attribute.getKeyAsString()), - attribute -> attribute.getValue().getValue() - )); + .filter(attribute -> isNamespaceDefinitionAttribute(attribute.getKeyAsString())) + .map(attribute -> new PrefixUri( + extractPrefixFromNamespaceDefinition(attribute.getKeyAsString()), + attribute.getValueAsString() + )) + .distinct() + .collect(Collectors.toMap(PrefixUri::getPrefix, PrefixUri::getUri)); } - public static Optional findNamespacePrefix(Cursor cursor, String namespacePrefix) { - String resolvedNamespace = null; + /** + * Gets a map containing all namespaces defined in the current scope, including all parent scopes. + * + * @param cursor the cursor to search from + * @param currentTag the current tag + * @return a map containing all namespaces defined in the current scope, including all parent scopes. + */ + public static Map findNamespaces(Cursor cursor, @Nullable Xml.Tag currentTag) { + Map namespaces = new HashMap<>(); + if (currentTag != null) { + namespaces.putAll(currentTag.getNamespaces()); + } + while (cursor != null) { Xml.Tag enclosing = cursor.firstEnclosing(Xml.Tag.class); if (enclosing != null) { - resolvedNamespace = enclosing.getNamespaces().get(namespacePrefix); - break; + 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 Optional.ofNullable(resolvedNamespace); + return namespaces; + } + + /** + * Find the tag that contains the declaration of the {@link #XML_SCHEMA_INSTANCE_URI} namespace. + * + * @param cursor the cursor to search from + * @param currentTag the current tag + * @return the tag that contains the declaration of the given namespace URI. + */ + public static Xml.Tag findTagContainingXmlSchemaInstanceNamespace(Cursor cursor, Xml.Tag currentTag) { + Xml.Tag tag = currentTag; + if (tag.getNamespaces().containsValue(XML_SCHEMA_INSTANCE_URI)) { + return tag; + } + while (cursor != null) { + if (cursor.getValue() instanceof Xml.Document) { + return ((Xml.Document) cursor.getValue()).getRoot(); + } + 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."); + } + + @Value + static class PrefixUri { + String prefix; + String uri; } } 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 3d34a80a9a3..f7d0797c4ea 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 @@ -315,7 +315,7 @@ public Tag withNamespaces(Map namespaces) { List attributes = this.attributes; if (attributes.isEmpty()) { for (Map.Entry ns : namespaces.entrySet()) { - String key = XmlNamespaceUtils.getAttributeNameFor(ns.getKey()); + String key = XmlNamespaceUtils.getAttributeNameForPrefix(ns.getKey()); attributes = ListUtils.concat(attributes, new Xml.Attribute( randomId(), "", @@ -343,7 +343,7 @@ public Tag withNamespaces(Map namespaces) { )); for (Map.Entry ns : namespaces.entrySet()) { - String key = XmlNamespaceUtils.getAttributeNameFor(ns.getKey()); + String key = XmlNamespaceUtils.getAttributeNameForPrefix(ns.getKey()); if (attributeByKey.containsKey(key)) { Xml.Attribute attribute = attributeByKey.get(key); if (!ns.getValue().equals(attribute.getValueAsString())) { @@ -355,7 +355,7 @@ public Tag withNamespaces(Map namespaces) { } else { attributes = ListUtils.concat(attributes, new Xml.Attribute( randomId(), - "", + " ", Markers.EMPTY, new Xml.Ident( randomId(), @@ -551,8 +551,7 @@ public Tag withContent(@Nullable List content) { * @return The namespace prefix for this tag, if any. */ public Optional getNamespacePrefix() { - String namespacePrefix = XmlNamespaceUtils.extractNamespacePrefix(name); - return Optional.ofNullable(namespacePrefix.isEmpty() ? null : namespacePrefix); + return Optional.of(XmlNamespaceUtils.extractNamespacePrefix(name)); } /** @@ -570,7 +569,7 @@ public Optional getNamespaceUri(Cursor cursor) { return Optional.of(namespaces.get(namespacePrefix)); } - return XmlNamespaceUtils.findNamespacePrefix(cursor, namespacePrefix); + return Optional.ofNullable(XmlNamespaceUtils.findNamespaces(cursor, null).get(namespacePrefix)); } @Override @@ -642,8 +641,10 @@ public String getPrefix() { * @return The namespace prefix for this attribute, if any. */ public Optional getNamespacePrefix() { - String namespacePrefix = XmlNamespaceUtils.extractNamespacePrefix(key.getName()); - return Optional.ofNullable(namespacePrefix.isEmpty() ? null : namespacePrefix); + if (XmlNamespaceUtils.isNamespaceDefinitionAttribute(key.getName())) { + return Optional.empty(); + } + return Optional.of(XmlNamespaceUtils.extractNamespacePrefix(key.getName())); } /** @@ -651,7 +652,7 @@ public Optional getNamespacePrefix() { */ public Optional getNamespaceUri(Cursor cursor) { Optional maybeNamespacePrefix = getNamespacePrefix(); - return maybeNamespacePrefix.flatMap(s -> XmlNamespaceUtils.findNamespacePrefix(cursor, s)); + return maybeNamespacePrefix.flatMap(s -> Optional.ofNullable(XmlNamespaceUtils.findNamespaces(cursor, null).get(s))); } @Override diff --git a/rewrite-xml/src/test/java/org/openrewrite/xml/internal/XmlNamespaceUtilsTest.java b/rewrite-xml/src/test/java/org/openrewrite/xml/internal/XmlNamespaceUtilsTest.java new file mode 100644 index 00000000000..2d19673423c --- /dev/null +++ b/rewrite-xml/src/test/java/org/openrewrite/xml/internal/XmlNamespaceUtilsTest.java @@ -0,0 +1,55 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.*; + +class XmlNamespaceUtilsTest { + + @Test + void isNamespaceDefinitionAttributeTests() { + assertTrue(XmlNamespaceUtils.isNamespaceDefinitionAttribute("xmlns:test")); + assertFalse(XmlNamespaceUtils.isNamespaceDefinitionAttribute("test")); + } + + @Test + void getAttributeNameForPrefix() { + assertEquals("xmlns:test", XmlNamespaceUtils.getAttributeNameForPrefix("test")); + assertEquals("xmlns", XmlNamespaceUtils.getAttributeNameForPrefix("")); + } + + @Test + void extractNamespacePrefix() { + assertEquals("test", XmlNamespaceUtils.extractNamespacePrefix("test:tag")); + assertEquals("", XmlNamespaceUtils.extractNamespacePrefix("tag")); + } + + @Test + void extractLocalName() { + assertEquals("tag", XmlNamespaceUtils.extractLocalName("test:tag")); + assertEquals("tag", XmlNamespaceUtils.extractLocalName("tag")); + } + + @Test + void extractPrefixFromNamespaceDefinition() { + assertEquals("test", XmlNamespaceUtils.extractPrefixFromNamespaceDefinition("xmlns:test")); + assertEquals("", XmlNamespaceUtils.extractPrefixFromNamespaceDefinition("xmlns")); + assertThrows(IllegalArgumentException.class, () -> XmlNamespaceUtils.extractPrefixFromNamespaceDefinition("test")); + assertThrows(IllegalArgumentException.class, () -> XmlNamespaceUtils.extractPrefixFromNamespaceDefinition("a:test")); + } +} \ No newline at end of file From 546b99f6c76195dc406eaadbce060dc315e9e4b4 Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Tue, 16 Jan 2024 22:55:52 -0500 Subject: [PATCH 10/21] `ChangeNamespaceValue` now updates the `schemaLocation` attribute --- .../openrewrite/xml/ChangeNamespaceValue.java | 123 ++++++++++++++++++ .../java/org/openrewrite/xml/tree/Xml.java | 5 + .../xml/ChangeNamespaceValueTest.java | 64 +++++++-- 3 files changed, 183 insertions(+), 9 deletions(-) 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 876b736fcb9..17bd171abf1 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,27 @@ 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.internal.XmlNamespaceUtils; 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 = true) 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() { @@ -71,16 +84,40 @@ public String getDescription() { 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; + @Override public TreeVisitor getVisitor() { XPathMatcher elementNameMatcher = elementName != null ? new XPathMatcher(elementName) : null; return new XmlIsoVisitor() { + @Override + public Xml.Document visitDocument(Xml.Document document, ExecutionContext executionContext) { + document = super.visitDocument(document, executionContext); + if (executionContext.pollMessage(MSG_TAG_UPDATED, false)) { + document = document.withRoot(addOrUpdateSchemaLocation(document.getRoot(), getCursor())); + } + return document; + } + @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 +151,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) || @@ -149,6 +198,80 @@ 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 = XmlNamespaceUtils.findTagContainingXmlSchemaInstanceNamespace(cursor, tag); + Map namespaces = tag.getNamespaces(); + return schemaLocationTag.getAttributes().stream().filter(attribute -> { + String attributeNamespace = namespaces.get(XmlNamespaceUtils.extractNamespacePrefix(attribute.getKeyAsString())); + return XmlNamespaceUtils.XML_SCHEMA_INSTANCE_URI.equals(attributeNamespace) + && attribute.getKeyAsString().endsWith("schemaLocation"); + }).findFirst(); + } + + private Xml.Tag maybeAddNamespace(Xml.Tag root) { + Map namespaces = root.getNamespaces(); + if (namespaces.containsValue(newValue) && !namespaces.containsValue(XmlNamespaceUtils.XML_SCHEMA_INSTANCE_URI)) { + namespaces.put(XmlNamespaceUtils.XML_SCHEMA_INSTANCE_PREFIX, XmlNamespaceUtils.XML_SCHEMA_INSTANCE_URI); + root = root.withNamespaces(namespaces); + } + return root; + } + + private Xml.Tag updateSchemaLocation(Xml.Tag newRoot, Xml.Attribute attribute) { + 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", XmlNamespaceUtils.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/tree/Xml.java b/rewrite-xml/src/main/java/org/openrewrite/xml/tree/Xml.java index f7d0797c4ea..30543da7b10 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 @@ -312,6 +312,11 @@ public Map getNamespaces() { } 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()) { 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 ce6805e0c2c..7d4e7a85005 100644 --- a/rewrite-xml/src/test/java/org/openrewrite/xml/ChangeNamespaceValueTest.java +++ b/rewrite-xml/src/test/java/org/openrewrite/xml/ChangeNamespaceValueTest.java @@ -25,7 +25,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)), xml( """ 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)), xml( """ 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)), xml( """ 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)), xml( """ 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)), xml( """ 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 + + """ + ) + ); + } } From f6213988b43bc87e63918632df085ab7651d2d57 Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Sat, 27 Jan 2024 11:45:02 -0500 Subject: [PATCH 11/21] Consider namespaces on `SemanticallyEqual`. --- .../openrewrite/xml/SemanticallyEqual.java | 69 ++++++++++++------- .../xml/internal/XmlNamespaceUtils.java | 2 +- .../java/org/openrewrite/xml/tree/Xml.java | 60 ++++++++++++---- .../xml/SemanticallyEqualTest.java | 27 ++++++++ 4 files changed, 118 insertions(+), 40 deletions(-) diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/SemanticallyEqual.java b/rewrite-xml/src/main/java/org/openrewrite/xml/SemanticallyEqual.java index 23ed0ec4c45..1614b4f4069 100755 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/SemanticallyEqual.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/SemanticallyEqual.java @@ -15,11 +15,13 @@ */ package org.openrewrite.xml; +import org.openrewrite.Cursor; import org.openrewrite.internal.lang.Nullable; import org.openrewrite.xml.tree.Content; import org.openrewrite.xml.tree.Xml; import java.util.List; +import java.util.Optional; import static java.util.Comparator.comparing; import static java.util.stream.Collectors.toList; @@ -46,51 +48,52 @@ private static class SemanticallyEqualVisitor extends XmlVisitor { @Override public Xml visitDocument(Xml.Document document, Xml other) { - if(document == other) { + if (document == other) { return null; } - if(!(other instanceof Xml.Document)) { + if (!(other instanceof Xml.Document)) { areEqual = false; return null; } - Xml.Document otherDocument = (Xml.Document)other; + Xml.Document otherDocument = (Xml.Document) other; visitTag(document.getRoot(), otherDocument.getRoot()); return null; } @Override public Xml visitTag(Xml.Tag tag, Xml other) { - if(tag == other) { + if (tag == other) { return null; } - if(!(other instanceof Xml.Tag)) { + if (!(other instanceof Xml.Tag)) { areEqual = false; return null; } - Xml.Tag otherTag = (Xml.Tag)other; - if (!tag.getName().equals(otherTag.getName())) { + Xml.Tag otherTag = (Xml.Tag) other; + if (!areTagsNamesEqual(tag, otherTag, getCursor())) { areEqual = false; return null; } - if(tag.getAttributes().size() != otherTag.getAttributes().size()) { + if (tag.getAttributes().size() != otherTag.getAttributes().size()) { areEqual = false; return null; } List theseAttributes = tag.getAttributes().stream() - .sorted(comparing(Xml.Attribute::getKeyAsString)) + .sorted(comparing(Xml.Attribute::getKeyLocalName).thenComparing(a -> a.getNamespaceUri(getCursor()), comparing(Optional::get))) .collect(toList()); List thoseAttributes = otherTag.getAttributes().stream() - .sorted(comparing(Xml.Attribute::getKeyAsString)) + .sorted(comparing(Xml.Attribute::getKeyLocalName).thenComparing(a -> a.getNamespaceUri(getCursor()), comparing(Optional::get))) .collect(toList()); - for(int i = 0; i < theseAttributes.size(); i++) { + + for (int i = 0; i < theseAttributes.size(); i++) { visitAttribute(theseAttributes.get(i), thoseAttributes.get(i)); - if(!areEqual) { + if (!areEqual) { return null; } } - if(bothNullOrEmpty(tag.getContent(), otherTag.getContent())) { + if (bothNullOrEmpty(tag.getContent(), otherTag.getContent())) { return null; - } else if(eitherNullOrEmpty(tag.getContent(), otherTag.getContent())) { + } else if (eitherNullOrEmpty(tag.getContent(), otherTag.getContent())) { areEqual = false; return null; } @@ -100,13 +103,13 @@ public Xml visitTag(Xml.Tag tag, Xml other) { List thoseContents = otherTag.getContent().stream() .filter(it -> it != null && !(it instanceof Xml.Comment)) .collect(toList()); - if(theseContents.size() != thoseContents.size()) { + if (theseContents.size() != thoseContents.size()) { areEqual = false; return null; } - for(int i = 0; i < theseContents.size(); i++) { + for (int i = 0; i < theseContents.size(); i++) { visit(theseContents.get(i), thoseContents.get(i)); - if(!areEqual) { + if (!areEqual) { return null; } } @@ -115,19 +118,19 @@ public Xml visitTag(Xml.Tag tag, Xml other) { @Override public Xml visitAttribute(Xml.Attribute attribute, Xml other) { - if(attribute == other) { + if (attribute == other) { return null; } - if(!(other instanceof Xml.Attribute)) { + if (!(other instanceof Xml.Attribute)) { areEqual = false; return null; } - Xml.Attribute otherAttribute = (Xml.Attribute)other; - if(!attribute.getKeyAsString().equals(otherAttribute.getKeyAsString())) { + Xml.Attribute otherAttribute = (Xml.Attribute) other; + if (!areAttributesNamesEqual(attribute, otherAttribute, getCursor())) { areEqual = false; return null; } - if(!attribute.getValueAsString().equals(otherAttribute.getValueAsString())) { + if (!attribute.getValueAsString().equals(otherAttribute.getValueAsString())) { areEqual = false; return null; } @@ -136,28 +139,42 @@ public Xml visitAttribute(Xml.Attribute attribute, Xml other) { @Override public Xml visitCharData(Xml.CharData charData, Xml other) { - if(charData == other) { + if (charData == other) { return null; } - if(!(other instanceof Xml.CharData)) { + if (!(other instanceof Xml.CharData)) { areEqual = false; return null; } - Xml.CharData otherChar = (Xml.CharData)other; - if(!charData.getText().trim().equals(otherChar.getText().trim())) { + Xml.CharData otherChar = (Xml.CharData) other; + if (!charData.getText().trim().equals(otherChar.getText().trim())) { areEqual = false; return null; } return null; } } + private static boolean isNullOrEmpty(@Nullable List a) { return a == null || a.isEmpty(); } + private static boolean bothNullOrEmpty(@Nullable List a, @Nullable List b) { return isNullOrEmpty(a) && isNullOrEmpty(b); } + private static boolean eitherNullOrEmpty(@Nullable List a, @Nullable List b) { return isNullOrEmpty(a) || isNullOrEmpty(b); } + + private static boolean areTagsNamesEqual(Xml.Tag tag, Xml.Tag other, Cursor cursor) { + return tag.getLocalName().equals(other.getLocalName()) && + tag.getNamespaceUri(cursor).equals(other.getNamespaceUri(cursor)); + } + + private static boolean areAttributesNamesEqual(Xml.Attribute attribute, Xml.Attribute other, Cursor cursor) { + return attribute.getKeyLocalName().equals(other.getKeyLocalName()) && + attribute.getValueAsString().equals(other.getValueAsString()) && + attribute.getNamespaceUri(cursor).equals(other.getNamespaceUri(cursor)); + } } diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlNamespaceUtils.java b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlNamespaceUtils.java index 4495fb45b0e..301944f06cb 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlNamespaceUtils.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlNamespaceUtils.java @@ -59,7 +59,7 @@ public static String extractNamespacePrefix(String name) { * @param name the tag or attribute name * @return the local name */ - static String extractLocalName(String name) { + public static String extractLocalName(String name) { int colon = name.indexOf(':'); return colon == -1 ? name : name.substring(colon + 1); } 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 30543da7b10..60d51c9aae3 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 @@ -21,6 +21,7 @@ import org.intellij.lang.annotations.Language; 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.XmlParser; @@ -552,11 +553,19 @@ public Tag withContent(@Nullable List content) { @With String beforeTagDelimiterPrefix; + /** + * @return The local name for this tag, without any namespace prefix. + */ + public String getLocalName() { + return XmlNamespaceUtils.extractLocalName(name); + } + /** * @return The namespace prefix for this tag, if any. */ public Optional getNamespacePrefix() { - return Optional.of(XmlNamespaceUtils.extractNamespacePrefix(name)); + String extractedNamespacePrefix = XmlNamespaceUtils.extractNamespacePrefix(name); + return Optional.ofNullable(StringUtils.isNotEmpty(extractedNamespacePrefix) ? extractedNamespacePrefix : null); } /** @@ -564,17 +573,7 @@ public Optional getNamespacePrefix() { */ public Optional getNamespaceUri(Cursor cursor) { Optional maybeNamespacePrefix = getNamespacePrefix(); - if (!maybeNamespacePrefix.isPresent()) { - return Optional.empty(); - } - - String namespacePrefix = maybeNamespacePrefix.get(); - Map namespaces = XmlNamespaceUtils.extractNamespaces(attributes); - if (namespaces.containsKey(namespacePrefix)) { - return Optional.of(namespaces.get(namespacePrefix)); - } - - return Optional.ofNullable(XmlNamespaceUtils.findNamespaces(cursor, null).get(namespacePrefix)); + return maybeNamespacePrefix.flatMap(s -> Optional.ofNullable(XmlNamespaceUtils.findNamespaces(cursor, null).get(s))); } @Override @@ -649,7 +648,8 @@ public Optional getNamespacePrefix() { if (XmlNamespaceUtils.isNamespaceDefinitionAttribute(key.getName())) { return Optional.empty(); } - return Optional.of(XmlNamespaceUtils.extractNamespacePrefix(key.getName())); + String extractedNamespacePrefix = XmlNamespaceUtils.extractNamespacePrefix(key.getName()); + return Optional.ofNullable(StringUtils.isNotEmpty(extractedNamespacePrefix) ? extractedNamespacePrefix : null); } /** @@ -695,6 +695,10 @@ public String getKeyAsString() { return key.getName(); } + public String getKeyLocalName() { + return key.getLocalName(); + } + public String getValueAsString() { return value.getValue(); } @@ -888,6 +892,36 @@ public

Xml acceptXml(XmlVisitor

v, P p) { return v.visitIdent(this, p); } + /** + * @return the namespace prefix of this ident (empty string for the default namespace) + */ + public Optional getNamespacePrefix() { + String extractedNamespacePrefix = XmlNamespaceUtils.extractNamespacePrefix(name); + return Optional.ofNullable(StringUtils.isNotEmpty(extractedNamespacePrefix) ? extractedNamespacePrefix : null); + } + + /** + * Extract the local name from a tag or attribute name. + * + * @param name the tag or attribute name + * @return the local name + */ + + public String getLocalName() { + return XmlNamespaceUtils.extractLocalName(name); + } + + /** + * @return The namespace URI of the root tag of this ident, if any. + */ + public Optional getNamespaceUri(Cursor cursor) { + Optional maybeNamespacePrefix = getNamespacePrefix(); + if (!maybeNamespacePrefix.isPresent()) { + return Optional.empty(); + } + return maybeNamespacePrefix.flatMap(s -> Optional.ofNullable(XmlNamespaceUtils.findNamespaces(cursor, null).get(s))); + } + @Override public String toString() { return "Ident{" + name + "}"; diff --git a/rewrite-xml/src/test/java/org/openrewrite/xml/SemanticallyEqualTest.java b/rewrite-xml/src/test/java/org/openrewrite/xml/SemanticallyEqualTest.java index fa8d51b078e..81e213d528a 100755 --- a/rewrite-xml/src/test/java/org/openrewrite/xml/SemanticallyEqualTest.java +++ b/rewrite-xml/src/test/java/org/openrewrite/xml/SemanticallyEqualTest.java @@ -158,6 +158,33 @@ void differentChildTagSameContent() { ); } + @Test + void differentNamespacePreifx() { + rewriteRun( + semanticallyEqual(true), + xml("foo"), + xml("foo") + ); + } + + @Test + void differentNamespaceUri() { + rewriteRun( + semanticallyEqual(false), + xml("foo"), + xml("foo") + ); + } + + @Test + void unusedNamespaces() { + rewriteRun( + semanticallyEqual(true), + xml("foo"), + xml("foo") + ); + } + private static Consumer semanticallyEqual(boolean isEqual) { return spec -> spec.beforeRecipe(sources -> assertThat(SemanticallyEqual.areEqual((Xml) sources.get(0), (Xml) sources.get(1))).isEqualTo(isEqual)); From 218827b6da8060a837b9a648d164a03e6196c288 Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Sat, 27 Jan 2024 15:00:43 -0500 Subject: [PATCH 12/21] Suggestions from code review. --- rewrite-xml/src/main/java/org/openrewrite/xml/tree/Xml.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 60d51c9aae3..4db3d2d810d 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 @@ -163,7 +163,6 @@ public Document withRoot(Tag root) { if (this.root == root) { return this; } - Map namespaces = XmlNamespaceUtils.extractNamespaces(root.getAttributes()); return new Document(id, sourcePath, prefixUnsafe, markers, charsetName, charsetBomMarked, checksum, fileAttributes, prolog, root, eof); } @@ -901,12 +900,10 @@ public Optional getNamespacePrefix() { } /** - * Extract the local name from a tag or attribute name. + * Extract the local name from the identifier. * - * @param name the tag or attribute name * @return the local name */ - public String getLocalName() { return XmlNamespaceUtils.extractLocalName(name); } From a4561981bba3a1d771906fe2953ad2e2629801d1 Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Sat, 27 Jan 2024 23:21:30 -0500 Subject: [PATCH 13/21] Update rewrite-xml/src/main/java/org/openrewrite/xml/ChangeNamespaceValue.java Co-authored-by: Knut Wannheden --- .../main/java/org/openrewrite/xml/ChangeNamespaceValue.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 17bd171abf1..0e340bd5fc3 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/ChangeNamespaceValue.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/ChangeNamespaceValue.java @@ -102,9 +102,9 @@ public TreeVisitor getVisitor() { XPathMatcher elementNameMatcher = elementName != null ? new XPathMatcher(elementName) : null; return new XmlIsoVisitor() { @Override - public Xml.Document visitDocument(Xml.Document document, ExecutionContext executionContext) { - document = super.visitDocument(document, executionContext); - if (executionContext.pollMessage(MSG_TAG_UPDATED, false)) { + public Xml.Document visitDocument(Xml.Document document, ExecutionContext ctx) { + document = super.visitDocument(document, ctx); + if (ctx.pollMessage(MSG_TAG_UPDATED, false)) { document = document.withRoot(addOrUpdateSchemaLocation(document.getRoot(), getCursor())); } return document; From b283aa039f0ee6ae6d32872c971a1ea5ae6d67dc Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Fri, 2 Feb 2024 15:56:32 -0500 Subject: [PATCH 14/21] Revert namespace comparison changes in `SemanticallyEqual`. --- .../openrewrite/xml/SemanticallyEqual.java | 69 +++++++------------ .../xml/SemanticallyEqualTest.java | 27 -------- 2 files changed, 26 insertions(+), 70 deletions(-) diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/SemanticallyEqual.java b/rewrite-xml/src/main/java/org/openrewrite/xml/SemanticallyEqual.java index 1614b4f4069..23ed0ec4c45 100755 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/SemanticallyEqual.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/SemanticallyEqual.java @@ -15,13 +15,11 @@ */ package org.openrewrite.xml; -import org.openrewrite.Cursor; import org.openrewrite.internal.lang.Nullable; import org.openrewrite.xml.tree.Content; import org.openrewrite.xml.tree.Xml; import java.util.List; -import java.util.Optional; import static java.util.Comparator.comparing; import static java.util.stream.Collectors.toList; @@ -48,52 +46,51 @@ private static class SemanticallyEqualVisitor extends XmlVisitor { @Override public Xml visitDocument(Xml.Document document, Xml other) { - if (document == other) { + if(document == other) { return null; } - if (!(other instanceof Xml.Document)) { + if(!(other instanceof Xml.Document)) { areEqual = false; return null; } - Xml.Document otherDocument = (Xml.Document) other; + Xml.Document otherDocument = (Xml.Document)other; visitTag(document.getRoot(), otherDocument.getRoot()); return null; } @Override public Xml visitTag(Xml.Tag tag, Xml other) { - if (tag == other) { + if(tag == other) { return null; } - if (!(other instanceof Xml.Tag)) { + if(!(other instanceof Xml.Tag)) { areEqual = false; return null; } - Xml.Tag otherTag = (Xml.Tag) other; - if (!areTagsNamesEqual(tag, otherTag, getCursor())) { + Xml.Tag otherTag = (Xml.Tag)other; + if (!tag.getName().equals(otherTag.getName())) { areEqual = false; return null; } - if (tag.getAttributes().size() != otherTag.getAttributes().size()) { + if(tag.getAttributes().size() != otherTag.getAttributes().size()) { areEqual = false; return null; } List theseAttributes = tag.getAttributes().stream() - .sorted(comparing(Xml.Attribute::getKeyLocalName).thenComparing(a -> a.getNamespaceUri(getCursor()), comparing(Optional::get))) + .sorted(comparing(Xml.Attribute::getKeyAsString)) .collect(toList()); List thoseAttributes = otherTag.getAttributes().stream() - .sorted(comparing(Xml.Attribute::getKeyLocalName).thenComparing(a -> a.getNamespaceUri(getCursor()), comparing(Optional::get))) + .sorted(comparing(Xml.Attribute::getKeyAsString)) .collect(toList()); - - for (int i = 0; i < theseAttributes.size(); i++) { + for(int i = 0; i < theseAttributes.size(); i++) { visitAttribute(theseAttributes.get(i), thoseAttributes.get(i)); - if (!areEqual) { + if(!areEqual) { return null; } } - if (bothNullOrEmpty(tag.getContent(), otherTag.getContent())) { + if(bothNullOrEmpty(tag.getContent(), otherTag.getContent())) { return null; - } else if (eitherNullOrEmpty(tag.getContent(), otherTag.getContent())) { + } else if(eitherNullOrEmpty(tag.getContent(), otherTag.getContent())) { areEqual = false; return null; } @@ -103,13 +100,13 @@ public Xml visitTag(Xml.Tag tag, Xml other) { List thoseContents = otherTag.getContent().stream() .filter(it -> it != null && !(it instanceof Xml.Comment)) .collect(toList()); - if (theseContents.size() != thoseContents.size()) { + if(theseContents.size() != thoseContents.size()) { areEqual = false; return null; } - for (int i = 0; i < theseContents.size(); i++) { + for(int i = 0; i < theseContents.size(); i++) { visit(theseContents.get(i), thoseContents.get(i)); - if (!areEqual) { + if(!areEqual) { return null; } } @@ -118,19 +115,19 @@ public Xml visitTag(Xml.Tag tag, Xml other) { @Override public Xml visitAttribute(Xml.Attribute attribute, Xml other) { - if (attribute == other) { + if(attribute == other) { return null; } - if (!(other instanceof Xml.Attribute)) { + if(!(other instanceof Xml.Attribute)) { areEqual = false; return null; } - Xml.Attribute otherAttribute = (Xml.Attribute) other; - if (!areAttributesNamesEqual(attribute, otherAttribute, getCursor())) { + Xml.Attribute otherAttribute = (Xml.Attribute)other; + if(!attribute.getKeyAsString().equals(otherAttribute.getKeyAsString())) { areEqual = false; return null; } - if (!attribute.getValueAsString().equals(otherAttribute.getValueAsString())) { + if(!attribute.getValueAsString().equals(otherAttribute.getValueAsString())) { areEqual = false; return null; } @@ -139,42 +136,28 @@ public Xml visitAttribute(Xml.Attribute attribute, Xml other) { @Override public Xml visitCharData(Xml.CharData charData, Xml other) { - if (charData == other) { + if(charData == other) { return null; } - if (!(other instanceof Xml.CharData)) { + if(!(other instanceof Xml.CharData)) { areEqual = false; return null; } - Xml.CharData otherChar = (Xml.CharData) other; - if (!charData.getText().trim().equals(otherChar.getText().trim())) { + Xml.CharData otherChar = (Xml.CharData)other; + if(!charData.getText().trim().equals(otherChar.getText().trim())) { areEqual = false; return null; } return null; } } - private static boolean isNullOrEmpty(@Nullable List a) { return a == null || a.isEmpty(); } - private static boolean bothNullOrEmpty(@Nullable List a, @Nullable List b) { return isNullOrEmpty(a) && isNullOrEmpty(b); } - private static boolean eitherNullOrEmpty(@Nullable List a, @Nullable List b) { return isNullOrEmpty(a) || isNullOrEmpty(b); } - - private static boolean areTagsNamesEqual(Xml.Tag tag, Xml.Tag other, Cursor cursor) { - return tag.getLocalName().equals(other.getLocalName()) && - tag.getNamespaceUri(cursor).equals(other.getNamespaceUri(cursor)); - } - - private static boolean areAttributesNamesEqual(Xml.Attribute attribute, Xml.Attribute other, Cursor cursor) { - return attribute.getKeyLocalName().equals(other.getKeyLocalName()) && - attribute.getValueAsString().equals(other.getValueAsString()) && - attribute.getNamespaceUri(cursor).equals(other.getNamespaceUri(cursor)); - } } diff --git a/rewrite-xml/src/test/java/org/openrewrite/xml/SemanticallyEqualTest.java b/rewrite-xml/src/test/java/org/openrewrite/xml/SemanticallyEqualTest.java index 81e213d528a..fa8d51b078e 100755 --- a/rewrite-xml/src/test/java/org/openrewrite/xml/SemanticallyEqualTest.java +++ b/rewrite-xml/src/test/java/org/openrewrite/xml/SemanticallyEqualTest.java @@ -158,33 +158,6 @@ void differentChildTagSameContent() { ); } - @Test - void differentNamespacePreifx() { - rewriteRun( - semanticallyEqual(true), - xml("foo"), - xml("foo") - ); - } - - @Test - void differentNamespaceUri() { - rewriteRun( - semanticallyEqual(false), - xml("foo"), - xml("foo") - ); - } - - @Test - void unusedNamespaces() { - rewriteRun( - semanticallyEqual(true), - xml("foo"), - xml("foo") - ); - } - private static Consumer semanticallyEqual(boolean isEqual) { return spec -> spec.beforeRecipe(sources -> assertThat(SemanticallyEqual.areEqual((Xml) sources.get(0), (Xml) sources.get(1))).isEqualTo(isEqual)); From bf3b6584aa5f8c8858d9310f49cac923edccb506 Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Tue, 27 Feb 2024 00:55:56 -0500 Subject: [PATCH 15/21] Adding a Namespaces abstraction --- .../openrewrite/xml/ChangeNamespaceValue.java | 10 +-- .../org/openrewrite/xml/XPathMatcher.java | 7 +++ .../openrewrite/xml/internal/Namespaces.java | 61 +++++++++++++++++++ .../xml/internal/XmlNamespaceUtils.java | 41 ++++++------- .../xml/search/HasNamespacePrefix.java | 4 +- .../xml/search/HasNamespaceUri.java | 4 +- .../java/org/openrewrite/xml/tree/Xml.java | 11 ++-- 7 files changed, 103 insertions(+), 35 deletions(-) create mode 100644 rewrite-xml/src/main/java/org/openrewrite/xml/internal/Namespaces.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 cdae102de72..00c0e3d6773 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/ChangeNamespaceValue.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/ChangeNamespaceValue.java @@ -22,10 +22,10 @@ import org.openrewrite.internal.StringUtils; import org.openrewrite.internal.lang.Nullable; import org.openrewrite.marker.Markers; +import org.openrewrite.xml.internal.Namespaces; import org.openrewrite.xml.internal.XmlNamespaceUtils; import org.openrewrite.xml.tree.Xml; -import java.util.Map; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -215,7 +215,7 @@ private Xml.Tag addOrUpdateSchemaLocation(Xml.Tag root, Cursor cursor) { private Optional maybeGetSchemaLocation(Cursor cursor, Xml.Tag tag) { Xml.Tag schemaLocationTag = XmlNamespaceUtils.findTagContainingXmlSchemaInstanceNamespace(cursor, tag); - Map namespaces = tag.getNamespaces(); + Namespaces namespaces = tag.getNamespaces(); return schemaLocationTag.getAttributes().stream().filter(attribute -> { String attributeNamespace = namespaces.get(XmlNamespaceUtils.extractNamespacePrefix(attribute.getKeyAsString())); return XmlNamespaceUtils.XML_SCHEMA_INSTANCE_URI.equals(attributeNamespace) @@ -224,9 +224,9 @@ private Optional maybeGetSchemaLocation(Cursor cursor, Xml.Tag ta } private Xml.Tag maybeAddNamespace(Xml.Tag root) { - Map namespaces = root.getNamespaces(); - if (namespaces.containsValue(newValue) && !namespaces.containsValue(XmlNamespaceUtils.XML_SCHEMA_INSTANCE_URI)) { - namespaces.put(XmlNamespaceUtils.XML_SCHEMA_INSTANCE_PREFIX, XmlNamespaceUtils.XML_SCHEMA_INSTANCE_URI); + Namespaces namespaces = root.getNamespaces(); + if (namespaces.containsUri(newValue) && !namespaces.containsUri(XmlNamespaceUtils.XML_SCHEMA_INSTANCE_URI)) { + namespaces.add(XmlNamespaceUtils.XML_SCHEMA_INSTANCE_PREFIX, XmlNamespaceUtils.XML_SCHEMA_INSTANCE_URI); root = root.withNamespaces(namespaces); } return root; 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 4b560acc70d..ff79f062916 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/XPathMatcher.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/XPathMatcher.java @@ -17,6 +17,7 @@ import org.openrewrite.internal.StringUtils; import org.openrewrite.Cursor; +import org.openrewrite.xml.internal.Namespaces; import org.openrewrite.xml.search.FindTags; import org.openrewrite.xml.tree.Xml; @@ -42,9 +43,15 @@ public class XPathMatcher { // Regular expression to support conditional tags like `plugin[artifactId='maven-compiler-plugin']` private static final Pattern PATTERN = Pattern.compile("([-\\w]+)\\[([-\\w]+)='([-\\w.]+)']"); private final String expression; + private final Namespaces namespaces; public XPathMatcher(String expression) { + this(expression, new Namespaces()); + } + + public XPathMatcher(String expression, Namespaces namespaces) { this.expression = expression; + this.namespaces = namespaces; } /** diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/Namespaces.java b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/Namespaces.java new file mode 100644 index 00000000000..15fff9a1f47 --- /dev/null +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/Namespaces.java @@ -0,0 +1,61 @@ +/* + * 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 lombok.Value; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +@Value +public class Namespaces { + + Map namespaces = new HashMap<>(); + + public Namespaces() { + } + + public Namespaces(String prefix, String uri) { + add(prefix, uri); + } + + public Namespaces add(String prefix, String uri) { + namespaces.put(prefix, uri); + return this; + } + + public Namespaces combine(Namespaces namespaces) { + this.namespaces.putAll(namespaces.getNamespaces()); + return this; + } + + public String get(String prefix) { + return namespaces.get(prefix); + } + + public boolean containsPrefix(String prefix) { + return namespaces.containsKey(prefix); + } + + public boolean containsUri(String uri) { + return namespaces.containsValue(uri); + } + + public Set> entrySet() { + return namespaces.entrySet(); + } +} diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlNamespaceUtils.java b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlNamespaceUtils.java index 301944f06cb..d57d1e1e644 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlNamespaceUtils.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlNamespaceUtils.java @@ -21,10 +21,7 @@ import org.openrewrite.xml.tree.Xml; import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; import java.util.Map; -import java.util.stream.Collectors; public class XmlNamespaceUtils { @@ -77,17 +74,19 @@ public static String extractPrefixFromNamespaceDefinition(String name) { return "xmlns".equals(name) ? "" : extractLocalName(name); } - public static Map extractNamespaces(Collection attributes) { - return attributes.isEmpty() - ? Collections.emptyMap() - : attributes.stream() - .filter(attribute -> isNamespaceDefinitionAttribute(attribute.getKeyAsString())) - .map(attribute -> new PrefixUri( - extractPrefixFromNamespaceDefinition(attribute.getKeyAsString()), - attribute.getValueAsString() - )) - .distinct() - .collect(Collectors.toMap(PrefixUri::getPrefix, PrefixUri::getUri)); + public static Namespaces extractNamespaces(Collection attributes) { + final Namespaces namespaces = new Namespaces(); + if (!attributes.isEmpty()) { + attributes.stream() + .filter(attribute -> isNamespaceDefinitionAttribute(attribute.getKeyAsString())) + .map(attribute -> new PrefixUri( + extractPrefixFromNamespaceDefinition(attribute.getKeyAsString()), + attribute.getValueAsString() + )) + .distinct() + .forEach(prefixUri -> namespaces.add(prefixUri.getPrefix(), prefixUri.getUri())); + } + return namespaces; } /** @@ -97,20 +96,20 @@ public static Map extractNamespaces(Collection at * @param currentTag the current tag * @return a map containing all namespaces defined in the current scope, including all parent scopes. */ - public static Map findNamespaces(Cursor cursor, @Nullable Xml.Tag currentTag) { - Map namespaces = new HashMap<>(); + public static Namespaces findNamespaces(Cursor cursor, @Nullable Xml.Tag currentTag) { + Namespaces namespaces = new Namespaces(); if (currentTag != null) { - namespaces.putAll(currentTag.getNamespaces()); + namespaces.combine(currentTag.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())) { + if (namespaces.containsUri(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()); + namespaces.add(ns.getKey(), ns.getValue()); } } cursor = cursor.getParent(); @@ -128,7 +127,7 @@ public static Map findNamespaces(Cursor cursor, @Nullable Xml.Ta */ public static Xml.Tag findTagContainingXmlSchemaInstanceNamespace(Cursor cursor, Xml.Tag currentTag) { Xml.Tag tag = currentTag; - if (tag.getNamespaces().containsValue(XML_SCHEMA_INSTANCE_URI)) { + if (tag.getNamespaces().containsUri(XML_SCHEMA_INSTANCE_URI)) { return tag; } while (cursor != null) { @@ -137,7 +136,7 @@ public static Xml.Tag findTagContainingXmlSchemaInstanceNamespace(Cursor cursor, } tag = cursor.firstEnclosing(Xml.Tag.class); if (tag != null) { - if (tag.getNamespaces().containsValue(XML_SCHEMA_INSTANCE_URI)) { + if (tag.getNamespaces().containsUri(XML_SCHEMA_INSTANCE_URI)) { return tag; } } diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/search/HasNamespacePrefix.java b/rewrite-xml/src/main/java/org/openrewrite/xml/search/HasNamespacePrefix.java index c0721d80669..07a4e0ee3c7 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/search/HasNamespacePrefix.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/search/HasNamespacePrefix.java @@ -65,7 +65,7 @@ public TreeVisitor getVisitor() { @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()))) { + if (tag.getNamespaces().containsPrefix(namespacePrefix) && (matcher == null || matcher.matches(getCursor()))) { t = SearchResult.found(t); } return t; @@ -79,7 +79,7 @@ public static Set find(Xml x, String namespacePrefix, @Nullable String new XmlVisitor>() { @Override public Xml visitTag(Xml.Tag tag, Set ts) { - if (tag.getNamespaces().containsKey(namespacePrefix) && (matcher == null || matcher.matches(getCursor()))) { + if (tag.getNamespaces().containsPrefix(namespacePrefix) && (matcher == null || matcher.matches(getCursor()))) { ts.add(tag); } return super.visitTag(tag, 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 index dda5ade8e7e..390ccf6d9aa 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/search/HasNamespaceUri.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/search/HasNamespaceUri.java @@ -65,7 +65,7 @@ public TreeVisitor getVisitor() { @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()))) { + if (tag.getNamespaces().containsUri(namespaceUri) && (matcher == null || matcher.matches(getCursor()))) { t = SearchResult.found(t); } return t; @@ -79,7 +79,7 @@ public static Set find(Xml x, String namespaceUri, @Nullable String xPa new XmlVisitor>() { @Override public Xml visitTag(Xml.Tag tag, Set ts) { - if (tag.getNamespaces().containsValue(namespaceUri) && (matcher == null || matcher.matches(getCursor()))) { + if (tag.getNamespaces().containsUri(namespaceUri) && (matcher == null || matcher.matches(getCursor()))) { ts.add(tag); } return super.visitTag(tag, 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 b751535a308..97802cdc8aa 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 @@ -27,6 +27,7 @@ import org.openrewrite.marker.Markers; import org.openrewrite.xml.XmlParser; import org.openrewrite.xml.XmlVisitor; +import org.openrewrite.xml.internal.Namespaces; import org.openrewrite.xml.internal.WithPrefix; import org.openrewrite.xml.internal.XmlNamespaceUtils; import org.openrewrite.xml.internal.XmlPrinter; @@ -98,7 +99,7 @@ class Document implements Xml, SourceFile { /** * @return a map of namespace prefixes (without the xmlns prefix) to URIs for this document. */ - public Map getNamespaces() { + public Namespaces getNamespaces() { if (root == null) { throw new IllegalStateException("Cannot get namespaces if root tag is null"); } @@ -106,7 +107,7 @@ public Map getNamespaces() { return root.getNamespaces(); } - public Document withNamespaces(Map namespaces) { + public Document withNamespaces(Namespaces namespaces) { if (root == null) { throw new IllegalStateException("Cannot add namespaces if root tag is null"); } @@ -318,12 +319,12 @@ class Tag implements Xml, Content { /** * @return a map of namespace prefixes (without the xmlns prefix) to URIs for this tag. */ - public Map getNamespaces() { + public Namespaces getNamespaces() { return XmlNamespaceUtils.extractNamespaces(attributes); } - public Tag withNamespaces(Map namespaces) { - Map currentNamespaces = getNamespaces(); + public Tag withNamespaces(Namespaces namespaces) { + Namespaces currentNamespaces = getNamespaces(); if (currentNamespaces.equals(namespaces)) { return this; } From 326b82494e902c72303e31c308f09c17b9acc6b1 Mon Sep 17 00:00:00 2001 From: Evie Lau Date: Thu, 13 Jun 2024 17:52:58 -0500 Subject: [PATCH 16/21] Add support for wildcard and local-name() --- .../org/openrewrite/xml/XPathMatcher.java | 38 ++++++++--- .../org/openrewrite/xml/XPathMatcherTest.java | 68 +++++++++++++++---- 2 files changed, 83 insertions(+), 23 deletions(-) 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 a10bf5aeb86..b1317079ed0 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/XPathMatcher.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/XPathMatcher.java @@ -38,7 +38,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 Namespaces namespaces; @@ -89,6 +89,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]; @@ -101,6 +104,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; @@ -108,7 +113,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; } @@ -183,7 +188,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; } @@ -198,7 +203,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; } } @@ -208,21 +213,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/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<>() { From 20e62122d02cb0d4004cf0ecf280574c91fc5ab3 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Fri, 14 Jun 2024 13:09:45 +0200 Subject: [PATCH 17/21] Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../java/org/openrewrite/xml/search/HasNamespacePrefix.java | 2 +- .../main/java/org/openrewrite/xml/search/HasNamespaceUri.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/search/HasNamespacePrefix.java b/rewrite-xml/src/main/java/org/openrewrite/xml/search/HasNamespacePrefix.java index 07a4e0ee3c7..47b5af94b7b 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/search/HasNamespacePrefix.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/search/HasNamespacePrefix.java @@ -32,7 +32,7 @@ import java.util.Set; @Value -@EqualsAndHashCode(callSuper = true) +@EqualsAndHashCode(callSuper = false) public class HasNamespacePrefix extends Recipe { @Option(displayName = "Namespace Prefix", 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 index 390ccf6d9aa..7791a3c22e5 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/search/HasNamespaceUri.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/search/HasNamespaceUri.java @@ -32,7 +32,7 @@ import java.util.Set; @Value -@EqualsAndHashCode(callSuper = true) +@EqualsAndHashCode(callSuper = false) public class HasNamespaceUri extends Recipe { @Option(displayName = "Namespace URI", From 69d677257a81e95fa31405b4a22455e9b68f3c7c Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Fri, 14 Jun 2024 17:38:14 -0400 Subject: [PATCH 18/21] Fix `Namespaces` mutability --- .../openrewrite/xml/ChangeNamespaceValue.java | 5 ++++- .../openrewrite/xml/internal/Namespaces.java | 20 ++++++++++++++----- .../xml/internal/XmlNamespaceUtils.java | 11 +++++----- 3 files changed, 25 insertions(+), 11 deletions(-) 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 00c0e3d6773..e26b9a04083 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/ChangeNamespaceValue.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/ChangeNamespaceValue.java @@ -178,6 +178,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) { @@ -226,7 +229,7 @@ private Optional maybeGetSchemaLocation(Cursor cursor, Xml.Tag ta private Xml.Tag maybeAddNamespace(Xml.Tag root) { Namespaces namespaces = root.getNamespaces(); if (namespaces.containsUri(newValue) && !namespaces.containsUri(XmlNamespaceUtils.XML_SCHEMA_INSTANCE_URI)) { - namespaces.add(XmlNamespaceUtils.XML_SCHEMA_INSTANCE_PREFIX, XmlNamespaceUtils.XML_SCHEMA_INSTANCE_URI); + namespaces = namespaces.add(XmlNamespaceUtils.XML_SCHEMA_INSTANCE_PREFIX, XmlNamespaceUtils.XML_SCHEMA_INSTANCE_URI); root = root.withNamespaces(namespaces); } return root; diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/Namespaces.java b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/Namespaces.java index 15fff9a1f47..6f00fbbaf6c 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/Namespaces.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/Namespaces.java @@ -30,17 +30,27 @@ public Namespaces() { } public Namespaces(String prefix, String uri) { - add(prefix, uri); + this.namespaces.put(prefix, uri); + } + + public Namespaces(Map namespaces) { + this.namespaces.putAll(namespaces); } public Namespaces add(String prefix, String uri) { - namespaces.put(prefix, uri); - return this; + Map combinedNamespaces = new HashMap<>(namespaces); + combinedNamespaces.put(prefix, uri); + return new Namespaces(combinedNamespaces); + } + + public Namespaces add(Map namespaces) { + Map combinedNamespaces = new HashMap<>(this.namespaces); + combinedNamespaces.putAll(namespaces); + return new Namespaces(combinedNamespaces); } public Namespaces combine(Namespaces namespaces) { - this.namespaces.putAll(namespaces.getNamespaces()); - return this; + return add(namespaces.getNamespaces()); } public String get(String prefix) { diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlNamespaceUtils.java b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlNamespaceUtils.java index d57d1e1e644..80c8bcba76e 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlNamespaceUtils.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlNamespaceUtils.java @@ -21,6 +21,7 @@ import org.openrewrite.xml.tree.Xml; import java.util.Collection; +import java.util.HashMap; import java.util.Map; public class XmlNamespaceUtils { @@ -75,7 +76,7 @@ public static String extractPrefixFromNamespaceDefinition(String name) { } public static Namespaces extractNamespaces(Collection attributes) { - final Namespaces namespaces = new Namespaces(); + final Map namespaces = new HashMap<>(attributes.size()); if (!attributes.isEmpty()) { attributes.stream() .filter(attribute -> isNamespaceDefinitionAttribute(attribute.getKeyAsString())) @@ -84,9 +85,9 @@ public static Namespaces extractNamespaces(Collection attributes) attribute.getValueAsString() )) .distinct() - .forEach(prefixUri -> namespaces.add(prefixUri.getPrefix(), prefixUri.getUri())); + .forEach(prefixUri -> namespaces.put(prefixUri.getPrefix(), prefixUri.getUri())); } - return namespaces; + return new Namespaces(namespaces); } /** @@ -99,7 +100,7 @@ public static Namespaces extractNamespaces(Collection attributes) public static Namespaces findNamespaces(Cursor cursor, @Nullable Xml.Tag currentTag) { Namespaces namespaces = new Namespaces(); if (currentTag != null) { - namespaces.combine(currentTag.getNamespaces()); + namespaces = namespaces.combine(currentTag.getNamespaces()); } while (cursor != null) { @@ -109,7 +110,7 @@ public static Namespaces findNamespaces(Cursor cursor, @Nullable Xml.Tag current if (namespaces.containsUri(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.add(ns.getKey(), ns.getValue()); + namespaces = namespaces.add(ns.getKey(), ns.getValue()); } } cursor = cursor.getParent(); From 2a0212b2dc55fba42a78956fd0798a7d764c3454 Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Fri, 14 Jun 2024 17:50:09 -0400 Subject: [PATCH 19/21] Adding an iterator implementation for `Namespaces` --- .../openrewrite/xml/internal/Namespaces.java | 35 ++++++++++++++++--- .../xml/internal/XmlNamespaceUtils.java | 2 +- .../java/org/openrewrite/xml/tree/Xml.java | 4 +-- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/Namespaces.java b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/Namespaces.java index 6f00fbbaf6c..0b81958dda1 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/Namespaces.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/Namespaces.java @@ -16,13 +16,14 @@ package org.openrewrite.xml.internal; import lombok.Value; +import org.jetbrains.annotations.NotNull; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; -import java.util.Set; @Value -public class Namespaces { +public class Namespaces implements Iterable> { Map namespaces = new HashMap<>(); @@ -65,7 +66,33 @@ public boolean containsUri(String uri) { return namespaces.containsValue(uri); } - public Set> entrySet() { - return namespaces.entrySet(); + @NotNull + @Override + public Iterator> iterator() { + return new NamespacesIterator(this.namespaces); + } + + private static class NamespacesIterator implements Iterator> { + + private final Iterator> entriesIterator; + + public NamespacesIterator(Map map) { + this.entriesIterator = map.entrySet().iterator(); + } + + @Override + public boolean hasNext() { + return entriesIterator.hasNext(); + } + + @Override + public Map.Entry next() { + return entriesIterator.next(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Removal not supported"); + } } } diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlNamespaceUtils.java b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlNamespaceUtils.java index 80c8bcba76e..45f85ba0dcf 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlNamespaceUtils.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlNamespaceUtils.java @@ -106,7 +106,7 @@ public static Namespaces findNamespaces(Cursor cursor, @Nullable Xml.Tag current while (cursor != null) { Xml.Tag enclosing = cursor.firstEnclosing(Xml.Tag.class); if (enclosing != null) { - for (Map.Entry ns : enclosing.getNamespaces().entrySet()) { + for (Map.Entry ns : enclosing.getNamespaces()) { if (namespaces.containsUri(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())); } 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 b8d7e7ca153..f93ddee2426 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 @@ -344,7 +344,7 @@ public Tag withNamespaces(Namespaces namespaces) { List attributes = this.attributes; if (attributes.isEmpty()) { - for (Map.Entry ns : namespaces.entrySet()) { + for (Map.Entry ns : namespaces) { String key = XmlNamespaceUtils.getAttributeNameForPrefix(ns.getKey()); attributes = ListUtils.concat(attributes, new Xml.Attribute( randomId(), @@ -372,7 +372,7 @@ public Tag withNamespaces(Namespaces namespaces) { a -> a )); - for (Map.Entry ns : namespaces.entrySet()) { + for (Map.Entry ns : namespaces) { String key = XmlNamespaceUtils.getAttributeNameForPrefix(ns.getKey()); if (attributeByKey.containsKey(key)) { Xml.Attribute attribute = attributeByKey.get(key); From cd2fcf3fc6f54c5822348ad543278ac3e69b06ac Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Fri, 14 Jun 2024 17:52:16 -0400 Subject: [PATCH 20/21] Replace `NotNull` with OpenRewrite's `NonNull` --- .../main/java/org/openrewrite/xml/internal/Namespaces.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/Namespaces.java b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/Namespaces.java index 0b81958dda1..58e967104f4 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/Namespaces.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/Namespaces.java @@ -16,7 +16,7 @@ package org.openrewrite.xml.internal; import lombok.Value; -import org.jetbrains.annotations.NotNull; +import org.openrewrite.internal.lang.NonNull; import java.util.HashMap; import java.util.Iterator; @@ -66,7 +66,7 @@ public boolean containsUri(String uri) { return namespaces.containsValue(uri); } - @NotNull + @NonNull @Override public Iterator> iterator() { return new NamespacesIterator(this.namespaces); From e4083b403ea818eb7d1facc7701dd42f7cf759bb Mon Sep 17 00:00:00 2001 From: Sam Snyder Date: Wed, 19 Jun 2024 21:41:06 -0700 Subject: [PATCH 21/21] 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 --- .../openrewrite/xml/ChangeNamespaceValue.java | 69 ++++-- .../org/openrewrite/xml/XPathMatcher.java | 7 - .../openrewrite/xml/internal/Namespaces.java | 98 -------- .../xml/internal/XmlNamespaceUtils.java | 156 ------------- ...cePrefix.java => FindNamespacePrefix.java} | 8 +- .../xml/search/HasNamespaceUri.java | 4 +- .../java/org/openrewrite/xml/tree/Xml.java | 210 ++++++++---------- .../xml/internal/XmlNamespaceUtilsTest.java | 55 ----- .../org/openrewrite/xml/internal/XmlTest.java | 57 +++++ ...Test.java => FindNamespacePrefixTest.java} | 12 +- 10 files changed, 216 insertions(+), 460 deletions(-) delete mode 100644 rewrite-xml/src/main/java/org/openrewrite/xml/internal/Namespaces.java delete mode 100644 rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlNamespaceUtils.java rename rewrite-xml/src/main/java/org/openrewrite/xml/search/{HasNamespacePrefix.java => FindNamespacePrefix.java} (89%) delete mode 100644 rewrite-xml/src/test/java/org/openrewrite/xml/internal/XmlNamespaceUtilsTest.java create mode 100644 rewrite-xml/src/test/java/org/openrewrite/xml/internal/XmlTest.java rename rewrite-xml/src/test/java/org/openrewrite/xml/search/{HasNamespacePrefixTest.java => FindNamespacePrefixTest.java} (90%) 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 e26b9a04083..6813ed343f2 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/ChangeNamespaceValue.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/ChangeNamespaceValue.java @@ -22,10 +22,9 @@ import org.openrewrite.internal.StringUtils; import org.openrewrite.internal.lang.Nullable; import org.openrewrite.marker.Markers; -import org.openrewrite.xml.internal.Namespaces; -import org.openrewrite.xml.internal.XmlNamespaceUtils; import org.openrewrite.xml.tree.Xml; +import java.util.Map; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -78,7 +77,7 @@ 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) @@ -90,24 +89,51 @@ public String getDescription() { example = "2.0") String newVersion; - @Option(displayName = "Schema Location", + @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) { - document = super.visitDocument(document, ctx); + Xml.Document d = super.visitDocument(document, ctx); if (ctx.pollMessage(MSG_TAG_UPDATED, false)) { - document = document.withRoot(addOrUpdateSchemaLocation(document.getRoot(), getCursor())); + d = d.withRoot(addOrUpdateSchemaLocation(d.getRoot(), getCursor())); } - return document; + return d; } @Override @@ -217,25 +243,32 @@ private Xml.Tag addOrUpdateSchemaLocation(Xml.Tag root, Cursor cursor) { } private Optional maybeGetSchemaLocation(Cursor cursor, Xml.Tag tag) { - Xml.Tag schemaLocationTag = XmlNamespaceUtils.findTagContainingXmlSchemaInstanceNamespace(cursor, tag); - Namespaces namespaces = tag.getNamespaces(); - return schemaLocationTag.getAttributes().stream().filter(attribute -> { - String attributeNamespace = namespaces.get(XmlNamespaceUtils.extractNamespacePrefix(attribute.getKeyAsString())); - return XmlNamespaceUtils.XML_SCHEMA_INSTANCE_URI.equals(attributeNamespace) - && attribute.getKeyAsString().endsWith("schemaLocation"); - }).findFirst(); + 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) { - Namespaces namespaces = root.getNamespaces(); - if (namespaces.containsUri(newValue) && !namespaces.containsUri(XmlNamespaceUtils.XML_SCHEMA_INSTANCE_URI)) { - namespaces = namespaces.add(XmlNamespaceUtils.XML_SCHEMA_INSTANCE_PREFIX, XmlNamespaceUtils.XML_SCHEMA_INSTANCE_URI); + 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); @@ -261,7 +294,7 @@ private Xml.Tag addSchemaLocation(Xml.Tag newRoot) { randomId(), "", Markers.EMPTY, - String.format("%s:schemaLocation", XmlNamespaceUtils.XML_SCHEMA_INSTANCE_PREFIX) + String.format("%s:schemaLocation", XML_SCHEMA_INSTANCE_PREFIX) ), "", new Xml.Attribute.Value( 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 b1317079ed0..7c6066b72f9 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/XPathMatcher.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/XPathMatcher.java @@ -18,7 +18,6 @@ import org.openrewrite.Cursor; import org.openrewrite.internal.StringUtils; import org.openrewrite.internal.lang.Nullable; -import org.openrewrite.xml.internal.Namespaces; import org.openrewrite.xml.search.FindTags; import org.openrewrite.xml.tree.Xml; @@ -41,18 +40,12 @@ public class XPathMatcher { private static final Pattern PATTERN = Pattern.compile("([-\\w]+|\\*)\\[((local-name|namespace-uri)\\(\\)|(@)?([-\\w]+|\\*))='([-\\w.]+)']"); private final String expression; - private final Namespaces namespaces; private final boolean startsWithSlash; private final boolean startsWithDoubleSlash; private final String[] parts; public XPathMatcher(String expression) { - this(expression, new Namespaces()); - } - - public XPathMatcher(String expression, Namespaces namespaces) { this.expression = expression; - this.namespaces = namespaces; startsWithSlash = expression.startsWith("/"); startsWithDoubleSlash = expression.startsWith("//"); parts = expression.substring(startsWithDoubleSlash ? 2 : startsWithSlash ? 1 : 0).split("/"); diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/Namespaces.java b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/Namespaces.java deleted file mode 100644 index 58e967104f4..00000000000 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/Namespaces.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * 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 lombok.Value; -import org.openrewrite.internal.lang.NonNull; - -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -@Value -public class Namespaces implements Iterable> { - - Map namespaces = new HashMap<>(); - - public Namespaces() { - } - - public Namespaces(String prefix, String uri) { - this.namespaces.put(prefix, uri); - } - - public Namespaces(Map namespaces) { - this.namespaces.putAll(namespaces); - } - - public Namespaces add(String prefix, String uri) { - Map combinedNamespaces = new HashMap<>(namespaces); - combinedNamespaces.put(prefix, uri); - return new Namespaces(combinedNamespaces); - } - - public Namespaces add(Map namespaces) { - Map combinedNamespaces = new HashMap<>(this.namespaces); - combinedNamespaces.putAll(namespaces); - return new Namespaces(combinedNamespaces); - } - - public Namespaces combine(Namespaces namespaces) { - return add(namespaces.getNamespaces()); - } - - public String get(String prefix) { - return namespaces.get(prefix); - } - - public boolean containsPrefix(String prefix) { - return namespaces.containsKey(prefix); - } - - public boolean containsUri(String uri) { - return namespaces.containsValue(uri); - } - - @NonNull - @Override - public Iterator> iterator() { - return new NamespacesIterator(this.namespaces); - } - - private static class NamespacesIterator implements Iterator> { - - private final Iterator> entriesIterator; - - public NamespacesIterator(Map map) { - this.entriesIterator = map.entrySet().iterator(); - } - - @Override - public boolean hasNext() { - return entriesIterator.hasNext(); - } - - @Override - public Map.Entry next() { - return entriesIterator.next(); - } - - @Override - public void remove() { - throw new UnsupportedOperationException("Removal not supported"); - } - } -} diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlNamespaceUtils.java b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlNamespaceUtils.java deleted file mode 100644 index 45f85ba0dcf..00000000000 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/XmlNamespaceUtils.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * 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 lombok.Value; -import org.openrewrite.Cursor; -import org.openrewrite.internal.lang.Nullable; -import org.openrewrite.xml.tree.Xml; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -public class XmlNamespaceUtils { - - public static final String XML_SCHEMA_INSTANCE_PREFIX = "xsi"; - public static final String XML_SCHEMA_INSTANCE_URI = "http://www.w3.org/2001/XMLSchema-instance"; - - private XmlNamespaceUtils() { - } - - public static boolean isNamespaceDefinitionAttribute(String name) { - return name.startsWith("xmlns"); - } - - public static String getAttributeNameForPrefix(String namespacePrefix) { - return namespacePrefix.isEmpty() ? "xmlns" : "xmlns:" + namespacePrefix; - } - - /** - * 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) - */ - public 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 - */ - public static String extractLocalName(String name) { - int colon = name.indexOf(':'); - return colon == -1 ? name : name.substring(colon + 1); - } - - /** - * Extract the namespace prefix from a namespace definition attribute name (xmlns* attributes). - * - * @param name the attribute name - * @return the namespace prefix - */ - public static String extractPrefixFromNamespaceDefinition(String name) { - if (!isNamespaceDefinitionAttribute(name)) { - throw new IllegalArgumentException("Namespace definition attribute names must start with \"xmlns\"."); - } - return "xmlns".equals(name) ? "" : extractLocalName(name); - } - - public static Namespaces extractNamespaces(Collection attributes) { - final Map namespaces = new HashMap<>(attributes.size()); - if (!attributes.isEmpty()) { - attributes.stream() - .filter(attribute -> isNamespaceDefinitionAttribute(attribute.getKeyAsString())) - .map(attribute -> new PrefixUri( - extractPrefixFromNamespaceDefinition(attribute.getKeyAsString()), - attribute.getValueAsString() - )) - .distinct() - .forEach(prefixUri -> namespaces.put(prefixUri.getPrefix(), prefixUri.getUri())); - } - return new Namespaces(namespaces); - } - - /** - * Gets a map containing all namespaces defined in the current scope, including all parent scopes. - * - * @param cursor the cursor to search from - * @param currentTag the current tag - * @return a map containing all namespaces defined in the current scope, including all parent scopes. - */ - public static Namespaces findNamespaces(Cursor cursor, @Nullable Xml.Tag currentTag) { - Namespaces namespaces = new Namespaces(); - if (currentTag != null) { - namespaces = namespaces.combine(currentTag.getNamespaces()); - } - - while (cursor != null) { - Xml.Tag enclosing = cursor.firstEnclosing(Xml.Tag.class); - if (enclosing != null) { - for (Map.Entry ns : enclosing.getNamespaces()) { - if (namespaces.containsUri(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 = namespaces.add(ns.getKey(), ns.getValue()); - } - } - cursor = cursor.getParent(); - } - - return namespaces; - } - - /** - * Find the tag that contains the declaration of the {@link #XML_SCHEMA_INSTANCE_URI} namespace. - * - * @param cursor the cursor to search from - * @param currentTag the current tag - * @return the tag that contains the declaration of the given namespace URI. - */ - public static Xml.Tag findTagContainingXmlSchemaInstanceNamespace(Cursor cursor, Xml.Tag currentTag) { - Xml.Tag tag = currentTag; - if (tag.getNamespaces().containsUri(XML_SCHEMA_INSTANCE_URI)) { - return tag; - } - while (cursor != null) { - if (cursor.getValue() instanceof Xml.Document) { - return ((Xml.Document) cursor.getValue()).getRoot(); - } - tag = cursor.firstEnclosing(Xml.Tag.class); - if (tag != null) { - if (tag.getNamespaces().containsUri(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."); - } - - @Value - static class PrefixUri { - String prefix; - String uri; - } -} diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/search/HasNamespacePrefix.java b/rewrite-xml/src/main/java/org/openrewrite/xml/search/FindNamespacePrefix.java similarity index 89% rename from rewrite-xml/src/main/java/org/openrewrite/xml/search/HasNamespacePrefix.java rename to rewrite-xml/src/main/java/org/openrewrite/xml/search/FindNamespacePrefix.java index 47b5af94b7b..95623523636 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/search/HasNamespacePrefix.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/search/FindNamespacePrefix.java @@ -33,9 +33,9 @@ @Value @EqualsAndHashCode(callSuper = false) -public class HasNamespacePrefix extends Recipe { +public class FindNamespacePrefix extends Recipe { - @Option(displayName = "Namespace Prefix", + @Option(displayName = "Namespace prefix", description = "The Namespace Prefix to find.", example = "http://www.w3.org/2001/XMLSchema-instance") String namespacePrefix; @@ -65,7 +65,7 @@ public TreeVisitor getVisitor() { @Override public Xml visitTag(Xml.Tag tag, ExecutionContext ctx) { Xml.Tag t = (Xml.Tag) super.visitTag(tag, ctx); - if (tag.getNamespaces().containsPrefix(namespacePrefix) && (matcher == null || matcher.matches(getCursor()))) { + if (tag.getNamespaces().containsKey(namespacePrefix) && (matcher == null || matcher.matches(getCursor()))) { t = SearchResult.found(t); } return t; @@ -79,7 +79,7 @@ public static Set find(Xml x, String namespacePrefix, @Nullable String new XmlVisitor>() { @Override public Xml visitTag(Xml.Tag tag, Set ts) { - if (tag.getNamespaces().containsPrefix(namespacePrefix) && (matcher == null || matcher.matches(getCursor()))) { + if (tag.getNamespaces().containsKey(namespacePrefix) && (matcher == null || matcher.matches(getCursor()))) { ts.add(tag); } return super.visitTag(tag, 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 index 7791a3c22e5..2745aff2264 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/search/HasNamespaceUri.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/search/HasNamespaceUri.java @@ -65,7 +65,7 @@ public TreeVisitor getVisitor() { @Override public Xml visitTag(Xml.Tag tag, ExecutionContext ctx) { Xml.Tag t = (Xml.Tag) super.visitTag(tag, ctx); - if (tag.getNamespaces().containsUri(namespaceUri) && (matcher == null || matcher.matches(getCursor()))) { + if (tag.getNamespaces().containsValue(namespaceUri) && (matcher == null || matcher.matches(getCursor()))) { t = SearchResult.found(t); } return t; @@ -79,7 +79,7 @@ public static Set find(Xml x, String namespaceUri, @Nullable String xPa new XmlVisitor>() { @Override public Xml visitTag(Xml.Tag tag, Set ts) { - if (tag.getNamespaces().containsUri(namespaceUri) && (matcher == null || matcher.matches(getCursor()))) { + if (tag.getNamespaces().containsValue(namespaceUri) && (matcher == null || matcher.matches(getCursor()))) { ts.add(tag); } return super.visitTag(tag, 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 f93ddee2426..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 @@ -27,20 +27,14 @@ import org.openrewrite.marker.Markers; import org.openrewrite.xml.XmlParser; import org.openrewrite.xml.XmlVisitor; -import org.openrewrite.xml.internal.Namespaces; import org.openrewrite.xml.internal.WithPrefix; -import org.openrewrite.xml.internal.XmlNamespaceUtils; import org.openrewrite.xml.internal.XmlPrinter; import org.openrewrite.xml.internal.XmlWhitespaceValidationService; 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.Map; -import java.util.Optional; -import java.util.UUID; +import java.util.*; import java.util.stream.Collectors; import static java.util.Collections.emptyList; @@ -80,42 +74,64 @@ 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; - /** - * @return a map of namespace prefixes (without the xmlns prefix) to URIs for this document. - */ - public Namespaces getNamespaces() { - if (root == null) { - throw new IllegalStateException("Cannot get namespaces if root tag is null"); - } - - return root.getNamespaces(); - } - - public Document withNamespaces(Namespaces namespaces) { - if (root == null) { - throw new IllegalStateException("Cannot add namespaces if root tag is null"); - } - - return withRoot(root.withNamespaces(namespaces)); - } - @Override public Document withPrefix(String prefix) { return WithPrefix.onlyIfNotEqual(this, prefix); @@ -126,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; @@ -154,16 +165,15 @@ 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 Tag root; public Document withRoot(Tag root) { @@ -173,7 +183,6 @@ public Document withRoot(Tag root) { return new Document(id, sourcePath, prefixUnsafe, markers, charsetName, charsetBomMarked, checksum, fileAttributes, prolog, root, eof); } - @Getter String eof; public Document withEof(String eof) { @@ -183,20 +192,6 @@ public Document withEof(String eof) { return new Document(id, sourcePath, prefixUnsafe, markers, charsetName, charsetBomMarked, checksum, fileAttributes, prolog, root, eof); } - /** - * @return The namespace prefix of the root tag of this document, if any. - */ - public Optional getNamespacePrefix() { - return root == null ? Optional.empty() : root.getNamespacePrefix(); - } - - /** - * @return The namespace URI of the root tag of this document, if any. - */ - public Optional getNamespaceUri(Cursor cursor) { - return root == null ? Optional.empty() : root.getNamespaceUri(cursor); - } - @Override public

Xml acceptXml(XmlVisitor

v, P p) { return v.visitDocument(this, p); @@ -319,6 +314,7 @@ public

Xml acceptXml(XmlVisitor

v, P p) { } } + @SuppressWarnings("unused") @Value @EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true) class Tag implements Xml, Content { @@ -330,22 +326,59 @@ class Tag implements Xml, Content { 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 Namespaces getNamespaces() { - return XmlNamespaceUtils.extractNamespaces(attributes); + 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(Namespaces namespaces) { - Namespaces currentNamespaces = getNamespaces(); + 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) { - String key = XmlNamespaceUtils.getAttributeNameForPrefix(ns.getKey()); + for (Map.Entry ns : namespaces.entrySet()) { + String key = getAttributeNameForPrefix(ns.getKey()); attributes = ListUtils.concat(attributes, new Xml.Attribute( randomId(), "", @@ -372,8 +405,8 @@ public Tag withNamespaces(Namespaces namespaces) { a -> a )); - for (Map.Entry ns : namespaces) { - String key = XmlNamespaceUtils.getAttributeNameForPrefix(ns.getKey()); + 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())) { @@ -589,14 +622,14 @@ public Tag withContent(@Nullable List content) { * @return The local name for this tag, without any namespace prefix. */ public String getLocalName() { - return XmlNamespaceUtils.extractLocalName(name); + return extractLocalName(name); } /** * @return The namespace prefix for this tag, if any. */ public Optional getNamespacePrefix() { - String extractedNamespacePrefix = XmlNamespaceUtils.extractNamespacePrefix(name); + String extractedNamespacePrefix = extractNamespacePrefix(name); return Optional.ofNullable(StringUtils.isNotEmpty(extractedNamespacePrefix) ? extractedNamespacePrefix : null); } @@ -605,7 +638,7 @@ public Optional getNamespacePrefix() { */ public Optional getNamespaceUri(Cursor cursor) { Optional maybeNamespacePrefix = getNamespacePrefix(); - return maybeNamespacePrefix.flatMap(s -> Optional.ofNullable(XmlNamespaceUtils.findNamespaces(cursor, null).get(s))); + return maybeNamespacePrefix.flatMap(s -> Optional.ofNullable(getAllNamespaces(cursor).get(s))); } @Override @@ -682,25 +715,6 @@ public String getPrefix() { String beforeEquals; Value value; - /** - * @return The namespace prefix for this attribute, if any. - */ - public Optional getNamespacePrefix() { - if (XmlNamespaceUtils.isNamespaceDefinitionAttribute(key.getName())) { - return Optional.empty(); - } - String extractedNamespacePrefix = XmlNamespaceUtils.extractNamespacePrefix(key.getName()); - return Optional.ofNullable(StringUtils.isNotEmpty(extractedNamespacePrefix) ? extractedNamespacePrefix : null); - } - - /** - * @return The namespace URI for this attribute, if any. - */ - public Optional getNamespaceUri(Cursor cursor) { - Optional maybeNamespacePrefix = getNamespacePrefix(); - return maybeNamespacePrefix.flatMap(s -> Optional.ofNullable(XmlNamespaceUtils.findNamespaces(cursor, null).get(s))); - } - @Override public

Xml acceptXml(XmlVisitor

v, P p) { return v.visitAttribute(this, p); @@ -743,10 +757,6 @@ public String getKeyAsString() { return key.getName(); } - public String getKeyLocalName() { - return key.getLocalName(); - } - public String getValueAsString() { return value.getValue(); } @@ -960,34 +970,6 @@ public

Xml acceptXml(XmlVisitor

v, P p) { return v.visitIdent(this, p); } - /** - * @return the namespace prefix of this ident (empty string for the default namespace) - */ - public Optional getNamespacePrefix() { - String extractedNamespacePrefix = XmlNamespaceUtils.extractNamespacePrefix(name); - return Optional.ofNullable(StringUtils.isNotEmpty(extractedNamespacePrefix) ? extractedNamespacePrefix : null); - } - - /** - * Extract the local name from the identifier. - * - * @return the local name - */ - public String getLocalName() { - return XmlNamespaceUtils.extractLocalName(name); - } - - /** - * @return The namespace URI of the root tag of this ident, if any. - */ - public Optional getNamespaceUri(Cursor cursor) { - Optional maybeNamespacePrefix = getNamespacePrefix(); - if (!maybeNamespacePrefix.isPresent()) { - return Optional.empty(); - } - return maybeNamespacePrefix.flatMap(s -> Optional.ofNullable(XmlNamespaceUtils.findNamespaces(cursor, null).get(s))); - } - @Override public String toString() { return "Ident{" + name + "}"; diff --git a/rewrite-xml/src/test/java/org/openrewrite/xml/internal/XmlNamespaceUtilsTest.java b/rewrite-xml/src/test/java/org/openrewrite/xml/internal/XmlNamespaceUtilsTest.java deleted file mode 100644 index 2d19673423c..00000000000 --- a/rewrite-xml/src/test/java/org/openrewrite/xml/internal/XmlNamespaceUtilsTest.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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 static org.junit.jupiter.api.Assertions.*; - -class XmlNamespaceUtilsTest { - - @Test - void isNamespaceDefinitionAttributeTests() { - assertTrue(XmlNamespaceUtils.isNamespaceDefinitionAttribute("xmlns:test")); - assertFalse(XmlNamespaceUtils.isNamespaceDefinitionAttribute("test")); - } - - @Test - void getAttributeNameForPrefix() { - assertEquals("xmlns:test", XmlNamespaceUtils.getAttributeNameForPrefix("test")); - assertEquals("xmlns", XmlNamespaceUtils.getAttributeNameForPrefix("")); - } - - @Test - void extractNamespacePrefix() { - assertEquals("test", XmlNamespaceUtils.extractNamespacePrefix("test:tag")); - assertEquals("", XmlNamespaceUtils.extractNamespacePrefix("tag")); - } - - @Test - void extractLocalName() { - assertEquals("tag", XmlNamespaceUtils.extractLocalName("test:tag")); - assertEquals("tag", XmlNamespaceUtils.extractLocalName("tag")); - } - - @Test - void extractPrefixFromNamespaceDefinition() { - assertEquals("test", XmlNamespaceUtils.extractPrefixFromNamespaceDefinition("xmlns:test")); - assertEquals("", XmlNamespaceUtils.extractPrefixFromNamespaceDefinition("xmlns")); - assertThrows(IllegalArgumentException.class, () -> XmlNamespaceUtils.extractPrefixFromNamespaceDefinition("test")); - assertThrows(IllegalArgumentException.class, () -> XmlNamespaceUtils.extractPrefixFromNamespaceDefinition("a:test")); - } -} \ No newline at end of file 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/HasNamespacePrefixTest.java b/rewrite-xml/src/test/java/org/openrewrite/xml/search/FindNamespacePrefixTest.java similarity index 90% rename from rewrite-xml/src/test/java/org/openrewrite/xml/search/HasNamespacePrefixTest.java rename to rewrite-xml/src/test/java/org/openrewrite/xml/search/FindNamespacePrefixTest.java index 4ebe87a9bbb..2e76cdb0c49 100644 --- a/rewrite-xml/src/test/java/org/openrewrite/xml/search/HasNamespacePrefixTest.java +++ b/rewrite-xml/src/test/java/org/openrewrite/xml/search/FindNamespacePrefixTest.java @@ -24,13 +24,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.openrewrite.xml.Assertions.xml; -class HasNamespacePrefixTest implements RewriteTest { +class FindNamespacePrefixTest implements RewriteTest { @DocumentExample @Test void rootElement() { rewriteRun( - spec -> spec.recipe(new HasNamespacePrefix("xsi", null)), + spec -> spec.recipe(new FindNamespacePrefix("xsi", null)), xml( source, """ @@ -54,7 +54,7 @@ void rootElement() { @Test void nestedElement() { rewriteRun( - spec -> spec.recipe(new HasNamespacePrefix("jaxws", null)), + spec -> spec.recipe(new FindNamespacePrefix("jaxws", null)), xml( source, """ @@ -78,7 +78,7 @@ void nestedElement() { @Test void noMatchOnNamespacePrefix() { rewriteRun( - spec -> spec.recipe(new HasNamespacePrefix("foo", null)), + spec -> spec.recipe(new FindNamespacePrefix("foo", null)), xml(source) ); } @@ -86,7 +86,7 @@ void noMatchOnNamespacePrefix() { @Test void noMatchOnXPath() { rewriteRun( - spec -> spec.recipe(new HasNamespacePrefix("xsi", "/jaxws:client")), + spec -> spec.recipe(new FindNamespacePrefix("xsi", "/jaxws:client")), xml(source) ); } @@ -96,7 +96,7 @@ void staticFind() { rewriteRun( xml( source, - spec -> spec.beforeRecipe(xml -> assertThat(HasNamespacePrefix.find(xml, "xsi", null)) + spec -> spec.beforeRecipe(xml -> assertThat(FindNamespacePrefix.find(xml, "xsi", null)) .isNotEmpty() .hasSize(1) .hasOnlyElementsOfType(Xml.Tag.class)