From c24ec1c3cb53030afe9ff9a0391d62bc5a8c81a5 Mon Sep 17 00:00:00 2001 From: Adriano Machado <60320+ammachado@users.noreply.github.com> Date: Sun, 14 Jul 2024 10:20:06 -0400 Subject: [PATCH] Add XSLT transformation recipe (#3606) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add XSLT transformation recipe * Update rewrite-maven/src/main/java/org/openrewrite/maven/ChangePluginConfiguration.java Co-authored-by: Tim te Beek * Update rewrite-maven/src/main/java/org/openrewrite/maven/ChangePluginConfiguration.java Co-authored-by: Tim te Beek * Update rewrite-xml/src/main/java/org/openrewrite/xml/XsltTransformation.java Co-authored-by: Tim te Beek * Update rewrite-xml/src/main/java/org/openrewrite/xml/XsltTransformationVisitor.java Co-authored-by: Tim te Beek * Update rewrite-xml/src/main/java/org/openrewrite/xml/XsltTransformationVisitor.java Co-authored-by: Tim te Beek * Update rewrite-xml/src/main/java/org/openrewrite/xml/XsltTransformationVisitor.java Co-authored-by: Tim te Beek * Update rewrite-xml/src/main/java/org/openrewrite/xml/XsltTransformationVisitor.java Co-authored-by: Tim te Beek * Fix merge issues * Update build.gradle.kts * Fix compilation errors * Add example values as suggested * Complete XsltTransformation description * Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Shorten and harden either/or validation * Small changes for consistency * Validate that configuration is set at most once --------- Co-authored-by: Tim te Beek Co-authored-by: Jonathan Schnéider Co-authored-by: Tim te Beek Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../maven/ChangePluginConfiguration.java | 63 +++++- .../maven/ChangePluginConfigurationTest.java | 184 +++++++++++++++++- .../src/test/resources/changePlugin.xslt | 15 ++ .../openrewrite/xml/XsltTransformation.java | 100 ++++++++++ .../xml/XsltTransformationVisitor.java | 88 +++++++++ .../xml/XsltTransformationTest.java | 77 ++++++++ .../xml/XsltTransformationTest.xslt | 15 ++ 7 files changed, 525 insertions(+), 17 deletions(-) create mode 100644 rewrite-maven/src/test/resources/changePlugin.xslt create mode 100644 rewrite-xml/src/main/java/org/openrewrite/xml/XsltTransformation.java create mode 100644 rewrite-xml/src/main/java/org/openrewrite/xml/XsltTransformationVisitor.java create mode 100644 rewrite-xml/src/test/java/org/openrewrite/xml/XsltTransformationTest.java create mode 100644 rewrite-xml/src/test/resources/org/openrewrite/xml/XsltTransformationTest.xslt diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/ChangePluginConfiguration.java b/rewrite-maven/src/main/java/org/openrewrite/maven/ChangePluginConfiguration.java index 2e8ae470ae7..64aac2bb1ff 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/ChangePluginConfiguration.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/ChangePluginConfiguration.java @@ -18,16 +18,21 @@ import lombok.EqualsAndHashCode; import lombok.Value; import org.intellij.lang.annotations.Language; -import org.openrewrite.ExecutionContext; -import org.openrewrite.Option; -import org.openrewrite.Recipe; -import org.openrewrite.TreeVisitor; +import org.openrewrite.*; +import org.openrewrite.internal.StringUtils; import org.openrewrite.internal.lang.Nullable; import org.openrewrite.xml.XPathMatcher; +import org.openrewrite.xml.XsltTransformation; +import org.openrewrite.xml.XsltTransformationVisitor; import org.openrewrite.xml.tree.Xml; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; import java.util.Optional; +import java.util.stream.Stream; +import static java.util.Objects.requireNonNull; import static org.openrewrite.xml.AddOrUpdateChild.addOrUpdateChild; import static org.openrewrite.xml.FilterTagChildrenVisitor.filterChildren; @@ -54,6 +59,21 @@ public class ChangePluginConfiguration extends Recipe { @Nullable String configuration; + @Nullable + @Language("xml") + @Option(displayName = "XSLT Configuration transformation", + description = "The transformation to be applied on the element.", + example = "...", + required = false) + String xslt; + + @Nullable + @Option(displayName = "XSLT Configuration transformation classpath resource", + description = "The transformation to be applied on the element provided as a classpath resource.", + example = "/changePlugin.xslt", + required = false) + String xsltResource; + @Override public String getDisplayName() { return "Change Maven plugin configuration"; @@ -69,6 +89,12 @@ public String getDescription() { return "Apply the specified configuration to a Maven plugin. Will not add the plugin if it does not already exist in the pom."; } + @Override + public Validated validate() { + return super.validate().and(Validated.test("configuration", "Configuration set at most once", configuration, + cfg -> Stream.of(configuration, xslt, xsltResource).filter(StringUtils::isBlank).count() >= 2)); + } + @Override public TreeVisitor getVisitor() { return new MavenVisitor() { @@ -79,19 +105,28 @@ public Xml visitTag(Xml.Tag tag, ExecutionContext ctx) { Optional maybePlugin = plugins.getChildren().stream() .filter(plugin -> "plugin".equals(plugin.getName()) && - groupId.equals(plugin.getChildValue("groupId").orElse(null)) && - artifactId.equals(plugin.getChildValue("artifactId").orElse(null)) + groupId.equals(plugin.getChildValue("groupId").orElse(null)) && + artifactId.equals(plugin.getChildValue("artifactId").orElse(null)) ) .findAny(); if (maybePlugin.isPresent()) { Xml.Tag plugin = maybePlugin.get(); - if (configuration == null) { + if (configuration == null && xslt == null && xsltResource == null) { plugins = filterChildren(plugins, plugin, child -> !(child instanceof Xml.Tag && "configuration".equals(((Xml.Tag) child).getName()))); - } else { + } else if (configuration != null) { plugins = addOrUpdateChild(plugins, plugin, Xml.Tag.build("\n" + configuration + "\n"), getCursor().getParentOrThrow()); + } else { + // Implied that xslt or xsltResource is not null + Optional configurationTag = plugin.getChild("configuration"); + if (configurationTag.isPresent()) { + String xsltTransformation = loadResource(xslt, xsltResource); + plugins = addOrUpdateChild(plugins, plugin, + XsltTransformationVisitor.transformTag(configurationTag.get().printTrimmed(getCursor()), xsltTransformation), + getCursor().getParentOrThrow()); + } } } } @@ -99,4 +134,16 @@ public Xml visitTag(Xml.Tag tag, ExecutionContext ctx) { } }; } + + private static String loadResource(@Nullable String xslt, @Nullable String xsltResource) { + if (StringUtils.isBlank(xsltResource)) { + return requireNonNull(xslt); + } + try (InputStream is = XsltTransformation.class.getResourceAsStream(xsltResource)) { + assert is != null; + return StringUtils.readFully(is, Charset.defaultCharset()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } diff --git a/rewrite-maven/src/test/java/org/openrewrite/maven/ChangePluginConfigurationTest.java b/rewrite-maven/src/test/java/org/openrewrite/maven/ChangePluginConfigurationTest.java index ee210c5604b..a04c8a595a7 100644 --- a/rewrite-maven/src/test/java/org/openrewrite/maven/ChangePluginConfigurationTest.java +++ b/rewrite-maven/src/test/java/org/openrewrite/maven/ChangePluginConfigurationTest.java @@ -15,26 +15,41 @@ */ package org.openrewrite.maven; +import org.intellij.lang.annotations.Language; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.openrewrite.DocumentExample; +import org.openrewrite.internal.StringUtils; import org.openrewrite.test.RewriteTest; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.openrewrite.maven.Assertions.pomXml; class ChangePluginConfigurationTest implements RewriteTest { + @Language("xml") + private static String xslt; + + @BeforeAll + static void setup() { + xslt = StringUtils.readFully(ChangePluginConfigurationTest.class + .getResourceAsStream("/changePlugin.xslt")); + + assertFalse(StringUtils.isBlank(xslt)); + } + @DocumentExample @Test void removeConfiguration() { rewriteRun( - spec -> spec.recipe(new ChangePluginConfiguration("org.openrewrite.maven", "rewrite-maven-plugin", null)), + spec -> spec.recipe(new ChangePluginConfiguration("org.openrewrite.maven", "rewrite-maven-plugin", null, null, null)), pomXml( """ org.example foo 1.0 - + @@ -56,7 +71,7 @@ void removeConfiguration() { org.example foo 1.0 - + @@ -78,14 +93,16 @@ void addConfiguration() { spec -> spec.recipe(new ChangePluginConfiguration( "org.openrewrite.maven", "rewrite-maven-plugin", - "\norg.openrewrite.java.cleanup.UnnecessaryThrows\n")), + "\norg.openrewrite.java.cleanup.UnnecessaryThrows\n", + null, + null)), pomXml( """ org.example foo 1.0 - + @@ -102,7 +119,7 @@ void addConfiguration() { org.example foo 1.0 - + @@ -129,14 +146,16 @@ void replaceConfiguration() { spec -> spec.recipe(new ChangePluginConfiguration( "org.openrewrite.maven", "rewrite-maven-plugin", - "\norg.openrewrite.java.cleanup.UnnecessaryThrows\n")), + "\norg.openrewrite.java.cleanup.UnnecessaryThrows\n", + null, + null)), pomXml( """ org.example foo 1.0 - + @@ -154,7 +173,43 @@ void replaceConfiguration() { org.example foo 1.0 - + + + + + org.openrewrite.maven + rewrite-maven-plugin + 4.1.5 + + + org.openrewrite.java.cleanup.UnnecessaryThrows + + + + + + + """ + ) + ); + } + + @Test + void transformConfigurationFromInlineTransformation() { + rewriteRun( + spec -> spec.recipe(new ChangePluginConfiguration( + "org.openrewrite.maven", + "rewrite-maven-plugin", + null, + xslt, + null)), + pomXml( + """ + + org.example + foo + 1.0 + @@ -170,6 +225,117 @@ void replaceConfiguration() { + """, + """ + + org.example + foo + 1.0 + + + + + org.openrewrite.maven + rewrite-maven-plugin + 4.1.5 + + + org.openrewrite.java.cleanup.UnnecessaryThrows + + + + + + + """ + ) + ); + } + + @Test + void transformConfigurationFromClasspathResource() { + rewriteRun( + spec -> spec.recipe(new ChangePluginConfiguration( + "org.openrewrite.maven", + "rewrite-maven-plugin", + null, + null, + "/changePlugin.xslt")), + pomXml( + """ + + org.example + foo + 1.0 + + + + + org.openrewrite.maven + rewrite-maven-plugin + 4.1.5 + + + org.openrewrite.java.cleanup.UnnecessaryThrows + + + + + + + """, + """ + + org.example + foo + 1.0 + + + + + org.openrewrite.maven + rewrite-maven-plugin + 4.1.5 + + + org.openrewrite.java.cleanup.UnnecessaryThrows + + + + + + + """ + ) + ); + } + + @Test + void transformConfigurationNoOpWhenConfigurationMissing() { + rewriteRun( + spec -> spec.recipe(new ChangePluginConfiguration( + "org.openrewrite.maven", + "rewrite-maven-plugin", + null, + null, + null)), + pomXml( + """ + + org.example + foo + 1.0 + + + + + org.openrewrite.maven + rewrite-maven-plugin + 4.1.5 + + + + """ ) ); diff --git a/rewrite-maven/src/test/resources/changePlugin.xslt b/rewrite-maven/src/test/resources/changePlugin.xslt new file mode 100644 index 00000000000..ee4b5243104 --- /dev/null +++ b/rewrite-maven/src/test/resources/changePlugin.xslt @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/XsltTransformation.java b/rewrite-xml/src/main/java/org/openrewrite/xml/XsltTransformation.java new file mode 100644 index 00000000000..917ba8bf247 --- /dev/null +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/XsltTransformation.java @@ -0,0 +1,100 @@ +/* + * 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; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.intellij.lang.annotations.Language; +import org.openrewrite.*; +import org.openrewrite.internal.StringUtils; +import org.openrewrite.internal.lang.Nullable; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.Arrays; + +import static java.util.Objects.requireNonNull; + +@Value +@EqualsAndHashCode(callSuper = false) +public class XsltTransformation extends Recipe { + + @Nullable + @Language("xml") + @Option(displayName = "XSLT Configuration transformation", + description = "The transformation to be applied.", + example = "...", + required = false) + String xslt; + + @Nullable + @Option(displayName = "XSLT Configuration transformation classpath resource", + description = "Recipe transformation provided as a classpath resource.", + example = "/changePlugin.xslt", + required = false) + String xsltResource; + + @Option(displayName = "File pattern", + description = "A glob expression that can be used to constrain which directories or source files should be searched. " + + "Multiple patterns may be specified, separated by a semicolon `;`. " + + "If multiple patterns are supplied any of the patterns matching will be interpreted as a match.", + example = "**/*.xml") + String filePattern; + + @Override + public String getDisplayName() { + return "XSLT transformation"; + } + + @Override + public String getDescription() { + return "Apply the specified XSLT transformation on matching files."; + } + + @Override + public TreeVisitor getVisitor() { + TreeVisitor visitor = new XsltTransformationVisitor(loadResource(xslt, xsltResource)); + + @SuppressWarnings("unchecked") + TreeVisitor check = Preconditions.or(Arrays.stream(filePattern.split(";")) + .map(FindSourceFiles::new) + .map(FindSourceFiles::getVisitor) + .toArray(TreeVisitor[]::new)); + + return Preconditions.check(check, visitor); + } + + @Override + public Validated validate() { + return super.validate() + .and(Validated.test("xslt", "set either xslt or xsltResource, but not both", + xslt, s -> StringUtils.isBlank(s) != StringUtils.isBlank(xsltResource) && + !StringUtils.isBlank(loadResource(xslt, xsltResource)))); + } + + private static String loadResource(@Nullable String xslt, @Nullable String xsltResource) { + if (StringUtils.isBlank(xsltResource)) { + return requireNonNull(xslt); + } + try (InputStream is = XsltTransformation.class.getResourceAsStream(xsltResource)) { + assert is != null; + return StringUtils.readFully(is, Charset.defaultCharset()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/XsltTransformationVisitor.java b/rewrite-xml/src/main/java/org/openrewrite/xml/XsltTransformationVisitor.java new file mode 100644 index 00000000000..d441761620c --- /dev/null +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/XsltTransformationVisitor.java @@ -0,0 +1,88 @@ +/* + * Copyright 2023 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; + +import lombok.RequiredArgsConstructor; +import org.openrewrite.ExecutionContext; +import org.openrewrite.marker.AlreadyReplaced; +import org.openrewrite.marker.Marker; +import org.openrewrite.xml.tree.Xml; + +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Objects; + +import static org.openrewrite.Tree.randomId; + +@RequiredArgsConstructor +public class XsltTransformationVisitor extends XmlVisitor { + + private final String xslt; + + @Override + public Xml visitDocument(Xml.Document document, ExecutionContext executionContext) { + for (Marker marker : document.getMarkers().getMarkers()) { + if (marker instanceof AlreadyReplaced) { + AlreadyReplaced alreadyReplaced = (AlreadyReplaced) marker; + if (Objects.equals(xslt, alreadyReplaced.getReplace())) { + return document; + } + } + } + + Xml.Document d = (Xml.Document) super.visitDocument(document, executionContext); + d = transform(d, xslt); + return d.withMarkers(document.getMarkers().add(new AlreadyReplaced(randomId(), null, xslt))); + } + + private static Xml.Document transform(Xml.Document document, String xslt) { + try { + Source xsltSource = new StreamSource(new ByteArrayInputStream(xslt.getBytes())); + Transformer transformer = TransformerFactory.newInstance().newTransformer(xsltSource); + + String originalDocument = document.printAll(); + Source text = new StreamSource(new ByteArrayInputStream(originalDocument.getBytes())); + try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { + transformer.transform(text, new StreamResult(os)); + return document.withRoot(Xml.Tag.build(os.toString().replace("\r", ""))); + } + } catch (IOException | TransformerException e) { + throw new RuntimeException("XSLT transformation exception: " + e.getMessage(), e); + } + } + + public static Xml.Tag transformTag(String sourceConfiguration, String xslt) { + try { + Source xsltSource = new StreamSource(new ByteArrayInputStream(xslt.getBytes())); + Transformer transformer = TransformerFactory.newInstance().newTransformer(xsltSource); + + Source text = new StreamSource(new ByteArrayInputStream(sourceConfiguration.getBytes())); + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + transformer.transform(text, new StreamResult(baos)); + return Xml.Tag.build(baos.toString()); + } + } catch (IOException | TransformerException e) { + throw new RuntimeException("XSLT transformation exception: " + e.getMessage(), e); + } + } +} diff --git a/rewrite-xml/src/test/java/org/openrewrite/xml/XsltTransformationTest.java b/rewrite-xml/src/test/java/org/openrewrite/xml/XsltTransformationTest.java new file mode 100644 index 00000000000..57b53c8d9bf --- /dev/null +++ b/rewrite-xml/src/test/java/org/openrewrite/xml/XsltTransformationTest.java @@ -0,0 +1,77 @@ +/* + * 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; + +import org.intellij.lang.annotations.Language; +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.internal.StringUtils; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.xml.Assertions.xml; + +class XsltTransformationTest implements RewriteTest { + @DocumentExample + @Test + void transformFromParameter() { + @Language("xml") + String xslt = StringUtils.readFully(XsltTransformationTest.class + .getResourceAsStream("/org/openrewrite/xml/XsltTransformationTest.xslt")); + + rewriteRun( + spec -> spec.recipe(new XsltTransformation(xslt, null, "**/*.xml")), + xml( + """ + + + org.openrewrite.java.cleanup.UnnecessaryThrows + + + """, + """ + + + org.openrewrite.java.cleanup.UnnecessaryThrows + + + """ + ) + ); + } + + @Test + void transformFromClasspathResource() { + rewriteRun( + spec -> spec.recipe(new XsltTransformation(null, "/org/openrewrite/xml/XsltTransformationTest.xslt", "**/*.xml")), + xml( + """ + + + org.openrewrite.java.cleanup.UnnecessaryThrows + + + """, + """ + + + org.openrewrite.java.cleanup.UnnecessaryThrows + + + """ + ) + ); + } +} diff --git a/rewrite-xml/src/test/resources/org/openrewrite/xml/XsltTransformationTest.xslt b/rewrite-xml/src/test/resources/org/openrewrite/xml/XsltTransformationTest.xslt new file mode 100644 index 00000000000..ee4b5243104 --- /dev/null +++ b/rewrite-xml/src/test/resources/org/openrewrite/xml/XsltTransformationTest.xslt @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file