From cda2cfb7d6fed2d870c44073ca04a555708ea41b Mon Sep 17 00:00:00 2001 From: Sam Snyder Date: Thu, 11 Jul 2024 21:38:22 -0700 Subject: [PATCH] Adapt XML Namespaced to be a trait --- .../openrewrite/xml/ChangeNamespaceValue.java | 123 ++++++--- .../org/openrewrite/xml/XPathMatcher.java | 15 +- .../xml/search/FindNamespacePrefix.java | 46 ++-- .../xml/search/HasNamespaceUri.java | 46 ++-- .../org/openrewrite/xml/trait/Namespaced.java | 246 +++++++++++++++++ .../package-info.java} | 20 +- .../java/org/openrewrite/xml/tree/Xml.java | 249 +----------------- .../xml/search/FindNamespacePrefixTest.java | 6 +- .../NamespacedTest.java} | 29 +- 9 files changed, 398 insertions(+), 382 deletions(-) create mode 100644 rewrite-xml/src/main/java/org/openrewrite/xml/trait/Namespaced.java rename rewrite-xml/src/main/java/org/openrewrite/xml/{tree/Namespaced.java => trait/package-info.java} (63%) rename rewrite-xml/src/test/java/org/openrewrite/xml/{internal/XmlTest.java => trait/NamespacedTest.java} (50%) 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 6813ed343f2..bdc33d1573d 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/ChangeNamespaceValue.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/ChangeNamespaceValue.java @@ -22,12 +22,16 @@ import org.openrewrite.internal.StringUtils; import org.openrewrite.internal.lang.Nullable; import org.openrewrite.marker.Markers; +import org.openrewrite.xml.trait.Namespaced; import org.openrewrite.xml.tree.Xml; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import static org.openrewrite.Tree.randomId; @@ -38,7 +42,6 @@ public class ChangeNamespaceValue extends Recipe { 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() { @@ -99,30 +102,6 @@ public String getDescription() { 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; @@ -130,8 +109,8 @@ public TreeVisitor getVisitor() { @Override public Xml.Document visitDocument(Xml.Document document, ExecutionContext ctx) { Xml.Document d = super.visitDocument(document, ctx); - if (ctx.pollMessage(MSG_TAG_UPDATED, false)) { - d = d.withRoot(addOrUpdateSchemaLocation(d.getRoot(), getCursor())); + if (d != document) { + d = d.withRoot(addOrUpdateSchemaLocation(d.getRoot())); } return d; } @@ -143,7 +122,6 @@ public Xml.Tag visitTag(Xml.Tag tag, ExecutionContext 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; @@ -228,12 +206,13 @@ private boolean isVersionMatch(Xml.Attribute attribute) { return false; } - private Xml.Tag addOrUpdateSchemaLocation(Xml.Tag root, Cursor cursor) { + private Xml.Tag addOrUpdateSchemaLocation(Xml.Tag root) { if (StringUtils.isBlank(newSchemaLocation)) { return root; } Xml.Tag newRoot = maybeAddNamespace(root); - Optional maybeSchemaLocation = maybeGetSchemaLocation(cursor, newRoot); + Namespaced n = new Namespaced(new Cursor(null, newRoot)); + Optional maybeSchemaLocation = maybeGetSchemaLocation(n); if (maybeSchemaLocation.isPresent() && oldValue != null) { newRoot = updateSchemaLocation(newRoot, maybeSchemaLocation.get()); } else if (!maybeSchemaLocation.isPresent()) { @@ -242,11 +221,10 @@ private Xml.Tag addOrUpdateSchemaLocation(Xml.Tag root, Cursor cursor) { return newRoot; } - private Optional maybeGetSchemaLocation(Cursor cursor, Xml.Tag tag) { - Xml.Tag schemaLocationTag = findTagContainingXmlSchemaInstanceNamespace(cursor); - Map namespaces = tag.getNamespaces(); - for (Xml.Attribute attribute : schemaLocationTag.getAttributes()) { - String attributeNamespace = namespaces.get(Xml.extractNamespacePrefix(attribute.getKeyAsString())); + private Optional maybeGetSchemaLocation(Namespaced n) { + Map namespaces = n.getNamespaces(); + for (Xml.Attribute attribute : n.getAttributes()) { + String attributeNamespace = namespaces.get(Namespaced.extractNamespacePrefix(attribute.getKeyAsString())); if(XML_SCHEMA_INSTANCE_URI.equals(attributeNamespace) && attribute.getKeyAsString().endsWith("schemaLocation")) { return Optional.of(attribute); @@ -255,16 +233,83 @@ private Optional maybeGetSchemaLocation(Cursor cursor, Xml.Tag ta return Optional.empty(); } - private Xml.Tag maybeAddNamespace(Xml.Tag root) { - Map namespaces = root.getNamespaces(); + Namespaced n = new Namespaced(new Cursor(null, root)); + Map namespaces = n.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); + Map newNamespaces = new LinkedHashMap<>(namespaces); + newNamespaces.put(XML_SCHEMA_INSTANCE_PREFIX, XML_SCHEMA_INSTANCE_URI); + root = withNamespaces(root, newNamespaces); } return root; } + public Xml.Tag withNamespaces(Xml.Tag tag, Map namespaces) { + List attributes = tag.getAttributes(); + if (attributes.isEmpty()) { + for (Map.Entry ns : namespaces.entrySet()) { + String key = Namespaced.getAttributeNameForPrefix(ns.getKey()); + attributes = ListUtils.concat(attributes, new Xml.Attribute( + randomId(), + "", + Markers.EMPTY, + new Xml.Ident( + randomId(), + "", + Markers.EMPTY, + key + ), + "", + new Xml.Attribute.Value( + randomId(), + "", + Markers.EMPTY, + Xml.Attribute.Value.Quote.Double, ns.getValue() + ) + )); + } + } else { + Map attributeByKey = attributes.stream() + .collect(Collectors.toMap( + Xml.Attribute::getKeyAsString, + a -> a + )); + + for (Map.Entry ns : namespaces.entrySet()) { + String key = Namespaced.getAttributeNameForPrefix(ns.getKey()); + if (attributeByKey.containsKey(key)) { + Xml.Attribute attribute = attributeByKey.get(key); + if (!ns.getValue().equals(attribute.getValueAsString())) { + ListUtils.map(attributes, a -> a.getKeyAsString().equals(key) + ? attribute.withValue(new Xml.Attribute.Value(randomId(), "", Markers.EMPTY, Xml.Attribute.Value.Quote.Double, ns.getValue())) + : a + ); + } + } else { + attributes = ListUtils.concat(attributes, new Xml.Attribute( + randomId(), + " ", + Markers.EMPTY, + new Xml.Ident( + randomId(), + "", + Markers.EMPTY, + key + ), + "", + new Xml.Attribute.Value( + randomId(), + "", + Markers.EMPTY, + Xml.Attribute.Value.Quote.Double, ns.getValue() + ) + )); + } + } + } + return tag.withAttributes(attributes); + } + private Xml.Tag updateSchemaLocation(Xml.Tag newRoot, Xml.Attribute attribute) { if(oldValue == null) { return newRoot; 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 74905d9281e..b0a7955aac1 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/XPathMatcher.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/XPathMatcher.java @@ -19,7 +19,7 @@ import org.openrewrite.internal.StringUtils; import org.openrewrite.internal.lang.Nullable; import org.openrewrite.xml.search.FindTags; -import org.openrewrite.xml.tree.Namespaced; +import org.openrewrite.xml.trait.Namespaced; import org.openrewrite.xml.tree.Xml; import java.util.*; @@ -281,13 +281,13 @@ private String matchesElementWithConditionFunction(Matcher matcher, Xml.Tag tag, } else if (isFunctionCondition) { // [local-name()='name'] pattern if (isAttributeElement) { for (Xml.Attribute a : tag.getAttributes()) { - if (matchesElementAndFunction(a, cursor, element, selector, value)) { + if (matchesElementAndFunction(new Cursor(cursor, a), element, selector, value)) { matchCurrentCondition = true; break; } } } else { - matchCurrentCondition = matchesElementAndFunction(tag, cursor, element, selector, value); + matchCurrentCondition = matchesElementAndFunction(cursor, element, selector, value); } } else { // other [] conditions for (Xml.Tag t : FindTags.find(tag, selector)) { @@ -309,13 +309,14 @@ private String matchesElementWithConditionFunction(Matcher matcher, Xml.Tag tag, return stillMatchesConditions ? element : null; } - private static boolean matchesElementAndFunction(Namespaced tagOrAttribute, Cursor cursor, String element, String selector, String value) { - if (!element.equals("*") && !tagOrAttribute.getName().equals(element)) { + private static boolean matchesElementAndFunction(Cursor cursor, String element, String selector, String value) { + Namespaced namespaced = new Namespaced(cursor); + if (!element.equals("*") && !Objects.equals(namespaced.getName().orElse(null), element)) { return false; } else if (selector.equals("local-name()")) { - return tagOrAttribute.getLocalName().equals(value); + return Objects.equals(namespaced.getLocalName().orElse(null), value); } else if (selector.equals("namespace-uri()")) { - Optional nsUri = tagOrAttribute.getNamespaceUri(cursor); + Optional nsUri = namespaced.getNamespaceUri(); return nsUri.isPresent() && nsUri.get().equals(value); } return false; diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/search/FindNamespacePrefix.java b/rewrite-xml/src/main/java/org/openrewrite/xml/search/FindNamespacePrefix.java index 95623523636..c85203389ab 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/search/FindNamespacePrefix.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/search/FindNamespacePrefix.java @@ -21,14 +21,12 @@ 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.trait.Namespaced; import org.openrewrite.xml.tree.Xml; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Set; @Value @@ -59,32 +57,26 @@ public String getDescription() { @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; - } - }; + return Namespaced.matcher() + .xPath(xPath) + .prefix(namespacePrefix) + .asVisitor(n -> n.getTree() instanceof Xml.Tag ? + SearchResult.found(n.getTree()) : + n.getTree()); } 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); + Set ts = new LinkedHashSet<>(); + Namespaced.matcher() + .xPath(xPath) + .prefix(namespacePrefix) + .asVisitor(n -> + { + if (n.getTree() instanceof Xml.Tag) { + ts.add((Xml.Tag) n.getTree()); + } + return n.getTree(); + }).visit(x, 0); 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 index 2745aff2264..599dd1b2a1c 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 @@ -21,14 +21,12 @@ 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.trait.Namespaced; import org.openrewrite.xml.tree.Xml; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Set; @Value @@ -59,32 +57,26 @@ public String getDescription() { @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; - } - }; + return Namespaced.matcher() + .xPath(xPath) + .uri(namespaceUri) + .asVisitor(n -> n.getTree() instanceof Xml.Tag ? + SearchResult.found(n.getTree()) : + n.getTree()); } 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); + Set ts = new LinkedHashSet<>(); + Namespaced.matcher() + .xPath(xPath) + .uri(namespaceUri) + .asVisitor(n -> + { + if (n.getTree() instanceof Xml.Tag) { + ts.add((Xml.Tag) n.getTree()); + } + return n.getTree(); + }).visit(x, 0); return ts; } } diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/trait/Namespaced.java b/rewrite-xml/src/main/java/org/openrewrite/xml/trait/Namespaced.java new file mode 100644 index 00000000000..d97aadac149 --- /dev/null +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/trait/Namespaced.java @@ -0,0 +1,246 @@ +/* + * 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.trait; + +import lombok.Value; +import org.openrewrite.Cursor; +import org.openrewrite.internal.StringUtils; +import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.trait.SimpleTraitMatcher; +import org.openrewrite.trait.Trait; +import org.openrewrite.xml.XPathMatcher; +import org.openrewrite.xml.tree.Xml; + +import java.util.*; +import java.util.stream.Collectors; + +@Value +public class Namespaced implements Trait { + + public static final String XML_SCHEMA_INSTANCE_URI = "http://www.w3.org/2001/XMLSchema-instance"; + + Cursor cursor; + + public Optional getName() { + Optional maybeName = Optional.empty(); + if (cursor.getValue() instanceof Xml.Tag) { + maybeName = Optional.of(((Xml.Tag) cursor.getValue()).getName()); + } else if (cursor.getValue() instanceof Xml.Attribute) { + Xml.Attribute attribute = cursor.getValue(); + maybeName = Optional.of(attribute.getKey().getName()); + } + return maybeName; + } + + public Optional getLocalName() { + Optional maybeLocalName = Optional.empty(); + if (cursor.getValue() instanceof Xml.Tag) { + Xml.Tag tag = cursor.getValue(); + maybeLocalName = Optional.of(extractLocalName(tag.getName())); + } else if (cursor.getValue() instanceof Xml.Attribute) { + Xml.Attribute attribute = cursor.getValue(); + maybeLocalName = Optional.of(extractLocalName(attribute.getKeyAsString())); + } + return maybeLocalName; + } + + public Optional getNamespacePrefix() { + String extractedNamespacePrefix = null; + if (cursor.getValue() instanceof Xml.Tag) { + Xml.Tag tag = cursor.getValue(); + extractedNamespacePrefix = extractNamespacePrefix(tag.getName()); + } else if (cursor.getValue() instanceof Xml.Attribute) { + Xml.Attribute attribute = cursor.getValue(); + extractedNamespacePrefix = extractNamespacePrefix(attribute.getKeyAsString()); + } + return StringUtils.isBlank(extractedNamespacePrefix) ? + Optional.empty() : + Optional.of(extractedNamespacePrefix); + } + + public Optional getNamespaceUri() { + return getNamespacePrefix().map(s -> getAllNamespaces().get(s)); + } + + public Map getNamespaces() { + Map namespaces = Collections.emptyMap(); + if (cursor.getValue() instanceof Xml.Tag) { + Xml.Tag tag = cursor.getValue(); + if (!tag.getAttributes().isEmpty()) { + namespaces = new LinkedHashMap<>(tag.getAttributes().size()); + for (Xml.Attribute attribute : tag.getAttributes()) { + if (isNamespaceDefinitionAttribute(attribute.getKeyAsString())) { + namespaces.put( + extractPrefixFromNamespaceDefinition(attribute.getKeyAsString()), + attribute.getValueAsString()); + } + } + } + } else if (cursor.getValue() instanceof Xml.Attribute) { + Xml.Attribute attribute = cursor.getValue(); + namespaces = Collections.singletonMap( + extractPrefixFromNamespaceDefinition(attribute.getKeyAsString()), + attribute.getValueAsString()); + } + return namespaces; + } + + public List getSchemaLocations() { + if (cursor.getValue() instanceof Xml.Tag) { + Xml.Tag tag = cursor.getValue(); + if (tag.getAttributes().isEmpty()) { + return Collections.emptyList(); + } + List schemaLocations = new ArrayList<>(); + Map namespaces = getAllNamespaces(); + for (Xml.Attribute attribute : tag.getAttributes()) { + if (XML_SCHEMA_INSTANCE_URI.equals(namespaces.get(extractNamespacePrefix(attribute.getKeyAsString())))) { + schemaLocations.add(attribute); + } + } + return schemaLocations; + } + return Collections.emptyList(); + } + + public List getAttributes() { + if (cursor.getValue() instanceof Xml.Tag) { + Xml.Tag tag = cursor.getValue(); + return tag.getAttributes(); + } else if (cursor.getValue() instanceof Xml.Attribute) { + return Collections.singletonList(cursor.getValue()); + } + return Collections.emptyList(); + } + + public List attributePrefixes() { + return getAttributes().stream() + .map(Xml.Attribute::getKeyAsString) + .map(Namespaced::extractNamespacePrefix) + .filter(StringUtils::isNotEmpty) + .distinct() + .collect(Collectors.toList()); + + } + + /** + * @return All namespaces in the current scope, including those defined in parent scopes. + */ + public Map getAllNamespaces() { + Map result = new LinkedHashMap<>(getNamespaces()); + if (cursor.getParent() != null) { + result.putAll(new Namespaced(cursor.getParent()).getAllNamespaces()); + } + return result; + } + + 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 namespace definition attribute name (xmlns* attributes). + * + * @param name the attribute name or null if not a namespace definition attribute + * @return the namespace prefix + */ + public 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) + */ + 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); + } + + public static Matcher matcher() { + return new Matcher(); + } + + public static class Matcher extends SimpleTraitMatcher { + @Nullable + private String prefix; + + @Nullable + private String uri; + + @Nullable + private XPathMatcher xPath; + + public Matcher prefix(@Nullable String prefix) { + this.prefix = prefix; + return this; + } + + public Matcher uri(@Nullable String uri) { + this.uri = uri; + return this; + } + + public Matcher xPath(@Nullable String xPath) { + if(xPath != null) { + this.xPath = new XPathMatcher(xPath); + } + return this; + } + + public Matcher xPath(@Nullable XPathMatcher xPath) { + this.xPath = xPath; + return this; + } + + @Override + protected @Nullable Namespaced test(Cursor cursor) { + if (xPath != null && !xPath.matches(cursor)) { + return null; + } + Namespaced namespaced = new Namespaced(cursor); + if (uri != null || prefix != null) { + Map namespaces = namespaced.getNamespaces(); + if ((uri != null && !namespaces.containsValue(uri)) + || (prefix != null && !namespaces.containsKey(prefix))) { + return null; + } + } + + return namespaced; + } + } +} diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/tree/Namespaced.java b/rewrite-xml/src/main/java/org/openrewrite/xml/trait/package-info.java similarity index 63% rename from rewrite-xml/src/main/java/org/openrewrite/xml/tree/Namespaced.java rename to rewrite-xml/src/main/java/org/openrewrite/xml/trait/package-info.java index cfa0d366a0c..782f1525bc3 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/tree/Namespaced.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/trait/package-info.java @@ -13,21 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.openrewrite.xml.tree; +@NonNullApi +package org.openrewrite.xml.trait; -import org.openrewrite.Cursor; - -import java.util.Map; -import java.util.Optional; - -public interface Namespaced extends Xml { - String getName(); - - String getLocalName(); - - Optional getNamespacePrefix(); - - Optional getNamespaceUri(Cursor cursor); - - Map getAllNamespaces(Cursor cursor); -} +import org.openrewrite.internal.lang.NonNullApi; 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 e8485e70825..35170b1b20d 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,8 +20,6 @@ import org.apache.commons.text.StringEscapeUtils; import org.intellij.lang.annotations.Language; import org.openrewrite.*; -import org.openrewrite.internal.ListUtils; -import org.openrewrite.internal.StringUtils; import org.openrewrite.internal.WhitespaceValidationService; import org.openrewrite.internal.lang.Nullable; import org.openrewrite.marker.Markers; @@ -74,49 +72,6 @@ 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) @@ -311,7 +266,7 @@ public

Xml acceptXml(XmlVisitor

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

Xml acceptXml(XmlVisitor

v, P p) { return v.visitTag(this, p); @@ -692,7 +503,7 @@ public String toString() { @lombok.Value @EqualsAndHashCode(callSuper = false, onlyExplicitlyIncluded = true) @With - class Attribute implements Xml, Namespaced { + class Attribute implements Xml { @EqualsAndHashCode.Include UUID id; @@ -759,62 +570,6 @@ public String getValueAsString() { return value.getValue(); } - @Override - public String getName() { - return key.getName(); - } - - /** - * @return The local name for this attribute, without any namespace prefix. - */ - @Override - public String getLocalName() { - return extractLocalName(getKeyAsString()); - } - - /** - * @return The namespace prefix for this attribute, if any. - */ - @Override - public Optional getNamespacePrefix() { - String extractedNamespacePrefix = extractNamespacePrefix(getKeyAsString()); - return Optional.ofNullable(StringUtils.isNotEmpty(extractedNamespacePrefix) ? extractedNamespacePrefix : null); - } - - /** - * @return The namespace URI for this attribute, if any. - */ - @Override - public Optional getNamespaceUri(Cursor cursor) { - Optional maybeNamespacePrefix = getNamespacePrefix(); - return maybeNamespacePrefix.flatMap(s -> Optional.ofNullable(getAllNamespaces(cursor).get(s))); - } - - /** - * 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. - */ - @Override - public Map getAllNamespaces(Cursor cursor) { - Map namespaces = new HashMap<>(); - 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; - } - @Override public String toString() { return getKeyAsString() + "=" + getValueAsString(); diff --git a/rewrite-xml/src/test/java/org/openrewrite/xml/search/FindNamespacePrefixTest.java b/rewrite-xml/src/test/java/org/openrewrite/xml/search/FindNamespacePrefixTest.java index 2e76cdb0c49..a52e6f258f1 100644 --- a/rewrite-xml/src/test/java/org/openrewrite/xml/search/FindNamespacePrefixTest.java +++ b/rewrite-xml/src/test/java/org/openrewrite/xml/search/FindNamespacePrefixTest.java @@ -39,7 +39,7 @@ void rootElement() { xsi:schemaLocation=" http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> - + @@ -63,7 +63,7 @@ void nestedElement() { xsi:schemaLocation=" http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> - + @@ -112,7 +112,7 @@ void staticFind() { xsi:schemaLocation=" http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> - + diff --git a/rewrite-xml/src/test/java/org/openrewrite/xml/internal/XmlTest.java b/rewrite-xml/src/test/java/org/openrewrite/xml/trait/NamespacedTest.java similarity index 50% rename from rewrite-xml/src/test/java/org/openrewrite/xml/internal/XmlTest.java rename to rewrite-xml/src/test/java/org/openrewrite/xml/trait/NamespacedTest.java index d029151e856..3ab9b2d5c1c 100644 --- a/rewrite-xml/src/test/java/org/openrewrite/xml/internal/XmlTest.java +++ b/rewrite-xml/src/test/java/org/openrewrite/xml/trait/NamespacedTest.java @@ -13,45 +13,44 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.openrewrite.xml.internal; +package org.openrewrite.xml.trait; 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 { +class NamespacedTest { @Test void isNamespaceDefinitionAttributeTests() { - assertThat(Xml.isNamespaceDefinitionAttribute("xmlns:test")).isTrue(); - assertThat(Xml.isNamespaceDefinitionAttribute("test")).isFalse(); + assertThat(Namespaced.isNamespaceDefinitionAttribute("xmlns:test")).isTrue(); + assertThat(Namespaced.isNamespaceDefinitionAttribute("test")).isFalse(); } @Test void getAttributeNameForPrefix() { - assertThat(Xml.getAttributeNameForPrefix("test")).isEqualTo("xmlns:test"); - assertThat(Xml.getAttributeNameForPrefix("")).isEqualTo("xmlns"); + assertThat(Namespaced.getAttributeNameForPrefix("test")).isEqualTo("xmlns:test"); + assertThat(Namespaced.getAttributeNameForPrefix("")).isEqualTo("xmlns"); } @Test void extractNamespacePrefix() { - assertEquals("test", Xml.extractNamespacePrefix("test:tag")); - assertEquals("", Xml.extractNamespacePrefix("tag")); + assertEquals("test", Namespaced.extractNamespacePrefix("test:tag")); + assertEquals("", Namespaced.extractNamespacePrefix("tag")); } @Test void extractLocalName() { - assertEquals("tag", Xml.extractLocalName("test:tag")); - assertEquals("tag", Xml.extractLocalName("tag")); + assertEquals("tag", Namespaced.extractLocalName("test:tag")); + assertEquals("tag", Namespaced.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); + assertEquals("test", Namespaced.extractPrefixFromNamespaceDefinition("xmlns:test")); + assertEquals("", Namespaced.extractPrefixFromNamespaceDefinition("xmlns")); + assertThat(Namespaced.extractPrefixFromNamespaceDefinition("test")).isEqualTo(null); + assertThat(Namespaced.extractPrefixFromNamespaceDefinition("a:test")).isEqualTo(null); } }