diff --git a/.github/workflows/comment-pr.yml b/.github/workflows/comment-pr.yml new file mode 100644 index 00000000000..e559610258a --- /dev/null +++ b/.github/workflows/comment-pr.yml @@ -0,0 +1,15 @@ +name: comment-pr + +# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#using-data-from-the-triggering-workflow +on: + workflow_run: + workflows: ["receive-pr"] + types: + - completed + +# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ +# Since this pull request has write permissions on the target repo, we should **NOT** execute any untrusted code. +jobs: + post-suggestions: + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: openrewrite/gh-automation/.github/workflows/comment-pr.yml@main diff --git a/.github/workflows/receive-pr.yml b/.github/workflows/receive-pr.yml new file mode 100644 index 00000000000..da78a5594bd --- /dev/null +++ b/.github/workflows/receive-pr.yml @@ -0,0 +1,68 @@ +name: receive-pr + +on: + pull_request: + types: [opened, synchronize] + branches: + - main + +concurrency: + group: '${{ github.workflow }} @ ${{ github.ref }}' + cancel-in-progress: true + +# https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ +# Since this pull request receives untrusted code, we should **NOT** have any secrets in the environment. +jobs: + upload-patch: + uses: openrewrite/gh-automation/.github/workflows/receive-pr.yml@main + with: + recipe: 'org.openrewrite.recipes.OpenRewriteBestPracticesSubset' + rewrite_yml: | + --- + type: specs.openrewrite.org/v1beta/recipe + name: org.openrewrite.recipes.OpenRewriteBestPracticesSubset + displayName: OpenRewrite best practices + description: Best practices for OpenRewrite recipe development. + recipeList: + - org.openrewrite.recipes.JavaRecipeBestPracticesSubset + - org.openrewrite.recipes.RecipeTestingBestPracticesSubset + - org.openrewrite.recipes.RecipeNullabilityBestPractices + #- org.openrewrite.java.OrderImports + #- org.openrewrite.java.format.EmptyNewlineAtEndOfFile + - org.openrewrite.staticanalysis.InlineVariable + - org.openrewrite.staticanalysis.MissingOverrideAnnotation + - org.openrewrite.staticanalysis.UseDiamondOperator + --- + type: specs.openrewrite.org/v1beta/recipe + name: org.openrewrite.recipes.JavaRecipeBestPracticesSubset + displayName: Java Recipe best practices + description: Best practices for Java recipe development. + preconditions: + - org.openrewrite.java.search.FindTypes: + fullyQualifiedTypeName: org.openrewrite.Recipe + checkAssignability: true + recipeList: + - org.openrewrite.java.recipes.BlankLinesAroundFieldsWithAnnotations + - org.openrewrite.java.recipes.ExecutionContextParameterName + - org.openrewrite.java.recipes.MissingOptionExample + - org.openrewrite.java.recipes.RecipeEqualsAndHashCodeCallSuper + - org.openrewrite.java.recipes.UseTreeRandomId + - org.openrewrite.staticanalysis.NeedBraces + #- org.openrewrite.staticanalysis.RemoveSystemOutPrintln + --- + type: specs.openrewrite.org/v1beta/recipe + name: org.openrewrite.recipes.RecipeTestingBestPracticesSubset + displayName: Recipe testing best practices + description: Best practices for testing recipes. + preconditions: + - org.openrewrite.java.search.FindTypes: + fullyQualifiedTypeName: org.openrewrite.test.RewriteTest + checkAssignability: true + recipeList: + - org.openrewrite.java.recipes.RewriteTestClassesShouldNotBePublic + #- org.openrewrite.java.recipes.SelectRecipeExamples + - org.openrewrite.java.recipes.SourceSpecTextBlockIndentation + - org.openrewrite.java.testing.cleanup.RemoveTestPrefix + - org.openrewrite.java.testing.cleanup.TestsShouldNotBePublic + - org.openrewrite.staticanalysis.NeedBraces + - org.openrewrite.staticanalysis.RemoveSystemOutPrintln diff --git a/rewrite-core/src/main/java/org/openrewrite/DataTable.java b/rewrite-core/src/main/java/org/openrewrite/DataTable.java index 37c7f0ac131..a56ad5db870 100644 --- a/rewrite-core/src/main/java/org/openrewrite/DataTable.java +++ b/rewrite-core/src/main/java/org/openrewrite/DataTable.java @@ -37,10 +37,10 @@ public class DataTable { private final Class type; @Language("markdown") - private final String displayName; + private final @NlsRewrite.DisplayName String displayName; @Language("markdown") - private final String description; + private final @NlsRewrite.Description String description; @Setter private boolean enabled = true; diff --git a/rewrite-core/src/main/java/org/openrewrite/NlsRewrite.java b/rewrite-core/src/main/java/org/openrewrite/NlsRewrite.java new file mode 100644 index 00000000000..b1d54f9e386 --- /dev/null +++ b/rewrite-core/src/main/java/org/openrewrite/NlsRewrite.java @@ -0,0 +1,31 @@ +/* + * 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; + +import org.jetbrains.annotations.Nls; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +public class NlsRewrite { + @Target({ElementType.TYPE_USE, ElementType.PARAMETER, ElementType.METHOD}) + public @Nls(capitalization = Nls.Capitalization.Sentence) @interface DisplayName { + } + + @Target({ElementType.TYPE_USE, ElementType.PARAMETER, ElementType.METHOD}) + public @Nls(capitalization = Nls.Capitalization.Sentence) @interface Description { + } +} diff --git a/rewrite-core/src/main/java/org/openrewrite/Option.java b/rewrite-core/src/main/java/org/openrewrite/Option.java index 96b34b44ca3..27a00306e91 100644 --- a/rewrite-core/src/main/java/org/openrewrite/Option.java +++ b/rewrite-core/src/main/java/org/openrewrite/Option.java @@ -25,9 +25,13 @@ @Target({ElementType.FIELD, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Option { - @Language("markdown") String displayName() default ""; - @Language("markdown") String description() default ""; + @Language("markdown") @NlsRewrite.DisplayName String displayName() default ""; + + @Language("markdown") @NlsRewrite.Description String description() default ""; + String example() default ""; + String[] valid() default ""; + boolean required() default true; } diff --git a/rewrite-core/src/main/java/org/openrewrite/Recipe.java b/rewrite-core/src/main/java/org/openrewrite/Recipe.java index 33837da3d7d..849c6205308 100644 --- a/rewrite-core/src/main/java/org/openrewrite/Recipe.java +++ b/rewrite-core/src/main/java/org/openrewrite/Recipe.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import lombok.Setter; import org.intellij.lang.annotations.Language; +import org.jetbrains.annotations.Nls; import org.openrewrite.config.DataTableDescriptor; import org.openrewrite.config.OptionDescriptor; import org.openrewrite.config.RecipeDescriptor; @@ -103,7 +104,7 @@ public int maxCycles() { * @return The display name. */ @Language("markdown") - public abstract String getDisplayName(); + public abstract @NlsRewrite.DisplayName String getDisplayName(); /** * A human-readable display name for this recipe instance, including some descriptive @@ -179,7 +180,7 @@ public String getInstanceNameSuffix() { * @return The display name. */ @Language("markdown") - public abstract String getDescription(); + public abstract @NlsRewrite.Description String getDescription(); /** * A set of strings used for categorizing related recipes. For example diff --git a/rewrite-core/src/main/java/org/openrewrite/config/CategoryDescriptor.java b/rewrite-core/src/main/java/org/openrewrite/config/CategoryDescriptor.java index 68401002370..a7475f6da34 100644 --- a/rewrite-core/src/main/java/org/openrewrite/config/CategoryDescriptor.java +++ b/rewrite-core/src/main/java/org/openrewrite/config/CategoryDescriptor.java @@ -18,6 +18,7 @@ import lombok.Value; import lombok.With; import org.intellij.lang.annotations.Language; +import org.openrewrite.NlsRewrite; import java.util.Set; @@ -29,11 +30,13 @@ public class CategoryDescriptor { public static final int HIGHEST_PRECEDENCE = Integer.MAX_VALUE; @Language("markdown") + @NlsRewrite.DisplayName String displayName; String packageName; @Language("markdown") + @NlsRewrite.Description String description; Set tags; diff --git a/rewrite-core/src/main/java/org/openrewrite/config/ColumnDescriptor.java b/rewrite-core/src/main/java/org/openrewrite/config/ColumnDescriptor.java index 005e4dcf38b..351de35ec9c 100644 --- a/rewrite-core/src/main/java/org/openrewrite/config/ColumnDescriptor.java +++ b/rewrite-core/src/main/java/org/openrewrite/config/ColumnDescriptor.java @@ -17,6 +17,7 @@ import lombok.EqualsAndHashCode; import lombok.Value; +import org.openrewrite.NlsRewrite; import org.openrewrite.internal.lang.Nullable; @Value @@ -30,8 +31,10 @@ public class ColumnDescriptor { String type; @Nullable + @NlsRewrite.DisplayName String displayName; @Nullable + @NlsRewrite.Description String description; } diff --git a/rewrite-core/src/main/java/org/openrewrite/config/DataTableDescriptor.java b/rewrite-core/src/main/java/org/openrewrite/config/DataTableDescriptor.java index 86610b03bf8..1d0fb8fadda 100644 --- a/rewrite-core/src/main/java/org/openrewrite/config/DataTableDescriptor.java +++ b/rewrite-core/src/main/java/org/openrewrite/config/DataTableDescriptor.java @@ -17,6 +17,7 @@ import lombok.EqualsAndHashCode; import lombok.Value; +import org.openrewrite.NlsRewrite; import java.util.List; @@ -27,8 +28,10 @@ public class DataTableDescriptor { @EqualsAndHashCode.Include String name; + @NlsRewrite.DisplayName String displayName; + @NlsRewrite.Description String description; @EqualsAndHashCode.Include diff --git a/rewrite-core/src/main/java/org/openrewrite/config/DeclarativeRecipe.java b/rewrite-core/src/main/java/org/openrewrite/config/DeclarativeRecipe.java index 7b7c7aec9b8..55d47dca979 100644 --- a/rewrite-core/src/main/java/org/openrewrite/config/DeclarativeRecipe.java +++ b/rewrite-core/src/main/java/org/openrewrite/config/DeclarativeRecipe.java @@ -150,6 +150,7 @@ public String getDescription() { public TreeVisitor getVisitor() { return new TreeVisitor() { TreeVisitor p = precondition.get(); + @Override public boolean isAcceptable(SourceFile sourceFile, ExecutionContext ctx) { return p.isAcceptable(sourceFile, ctx); @@ -314,11 +315,17 @@ public void addValidation(Validated validated) { @Override public Validated validate() { - return Validated.test("initialization", - "initialize(..) must be called on DeclarativeRecipe prior to use.", - this, r -> initValidation != null) - .and(validation) - .and(initValidation); + Validated validated = Validated.none(); + + if (!uninitializedRecipes.isEmpty() && uninitializedRecipes.size() != recipeList.size()) { + validated = validated.and(Validated.invalid("initialization", recipeList, "DeclarativeRecipe must not contain uninitialized recipes. Be sure to call .initialize() on DeclarativeRecipe.")); + } + if (!uninitializedPreconditions.isEmpty() && uninitializedPreconditions.size() != preconditions.size()) { + validated = validated.and(Validated.invalid("initialization", preconditions, "DeclarativeRecipe must not contain uninitialized preconditions. Be sure to call .initialize() on DeclarativeRecipe.")); + } + + return validated.and(validation) + .and(initValidation == null ? Validated.none() : initValidation); } @Value diff --git a/rewrite-core/src/main/java/org/openrewrite/config/OptionDescriptor.java b/rewrite-core/src/main/java/org/openrewrite/config/OptionDescriptor.java index 2051af1c0b5..d010831f286 100644 --- a/rewrite-core/src/main/java/org/openrewrite/config/OptionDescriptor.java +++ b/rewrite-core/src/main/java/org/openrewrite/config/OptionDescriptor.java @@ -17,6 +17,7 @@ import lombok.EqualsAndHashCode; import lombok.Value; +import org.openrewrite.NlsRewrite; import org.openrewrite.internal.lang.Nullable; import java.util.List; @@ -32,9 +33,11 @@ public class OptionDescriptor { String type; @Nullable + @NlsRewrite.DisplayName String displayName; @Nullable + @NlsRewrite.Description String description; @Nullable diff --git a/rewrite-core/src/main/java/org/openrewrite/config/RecipeDescriptor.java b/rewrite-core/src/main/java/org/openrewrite/config/RecipeDescriptor.java index 4dcbf2d390e..07a9d731ffe 100644 --- a/rewrite-core/src/main/java/org/openrewrite/config/RecipeDescriptor.java +++ b/rewrite-core/src/main/java/org/openrewrite/config/RecipeDescriptor.java @@ -20,6 +20,7 @@ import lombok.With; import org.openrewrite.Contributor; import org.openrewrite.Maintainer; +import org.openrewrite.NlsRewrite; import org.openrewrite.internal.lang.Nullable; import java.net.URI; @@ -33,8 +34,10 @@ public class RecipeDescriptor { @EqualsAndHashCode.Include String name; + @NlsRewrite.DisplayName String displayName; + @NlsRewrite.Description String description; Set tags; diff --git a/rewrite-core/src/main/java/org/openrewrite/ipc/http/HttpSender.java b/rewrite-core/src/main/java/org/openrewrite/ipc/http/HttpSender.java index fe101629cf5..6417dfa7ce6 100644 --- a/rewrite-core/src/main/java/org/openrewrite/ipc/http/HttpSender.java +++ b/rewrite-core/src/main/java/org/openrewrite/ipc/http/HttpSender.java @@ -24,6 +24,7 @@ import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.time.Duration; import java.util.Base64; import java.util.LinkedHashMap; import java.util.Map; @@ -74,12 +75,16 @@ class Request { private final byte[] entity; private final Method method; private final Map requestHeaders; + private final Duration connectTimeout; + private final Duration readTimeout; - public Request(URL url, byte[] entity, Method method, Map requestHeaders) { + public Request(URL url, byte[] entity, Method method, Map requestHeaders, Duration connectTimeout, Duration readTimeout) { this.url = url; this.entity = entity; this.method = method; this.requestHeaders = requestHeaders; + this.connectTimeout = connectTimeout; + this.readTimeout = readTimeout; } public URL getUrl() { @@ -98,6 +103,14 @@ public Map getRequestHeaders() { return requestHeaders; } + public Duration getConnectTimeout() { + return connectTimeout; + } + + public Duration getReadTimeout() { + return readTimeout; + } + public static Builder build(String uri, HttpSender sender) { return new Builder(uri, sender); } @@ -125,6 +138,8 @@ public static class Builder { private URL url; private byte[] entity = new byte[0]; private Method method; + private Duration connectTimeout; + private Duration readTimeout; Builder(String url, HttpSender sender) { try { @@ -277,8 +292,9 @@ public final Builder compress() throws IOException { * @throws IOException If compression fails. */ public final Builder compressWhen(Supplier when) throws IOException { - if (when.get()) + if (when.get()) { return compress(); + } return this; } @@ -331,7 +347,17 @@ public Response send() { } public Request build() { - return new Request(url, entity, method, requestHeaders); + return new Request(url, entity, method, requestHeaders, connectTimeout, readTimeout); + } + + public Builder withConnectTimeout(Duration connectTimeout) { + this.connectTimeout = connectTimeout; + return this; + } + + public Builder withReadTimeout(Duration readTimeout) { + this.readTimeout = readTimeout; + return this; } } } diff --git a/rewrite-core/src/main/java/org/openrewrite/ipc/http/HttpUrlConnectionSender.java b/rewrite-core/src/main/java/org/openrewrite/ipc/http/HttpUrlConnectionSender.java index 661a5ec4a8c..38990e1e1e9 100644 --- a/rewrite-core/src/main/java/org/openrewrite/ipc/http/HttpUrlConnectionSender.java +++ b/rewrite-core/src/main/java/org/openrewrite/ipc/http/HttpUrlConnectionSender.java @@ -77,8 +77,16 @@ public Response send(Request request) { } else { con = (HttpURLConnection) request.getUrl().openConnection(); } - con.setConnectTimeout(connectTimeoutMs); - con.setReadTimeout(readTimeoutMs); + if (request.getConnectTimeout() != null) { + con.setConnectTimeout((int) request.getConnectTimeout().toMillis()); + } else { + con.setConnectTimeout(connectTimeoutMs); + } + if (request.getReadTimeout() != null) { + con.setReadTimeout((int) request.getReadTimeout().toMillis()); + } else { + con.setReadTimeout(readTimeoutMs); + } Method method = request.getMethod(); con.setRequestMethod(method.name()); diff --git a/rewrite-core/src/main/java/org/openrewrite/style/NamedStyles.java b/rewrite-core/src/main/java/org/openrewrite/style/NamedStyles.java index a0d7a1cb5ef..2bc13939ab5 100644 --- a/rewrite-core/src/main/java/org/openrewrite/style/NamedStyles.java +++ b/rewrite-core/src/main/java/org/openrewrite/style/NamedStyles.java @@ -19,6 +19,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; import lombok.With; +import org.openrewrite.NlsRewrite; import org.openrewrite.Tree; import org.openrewrite.Validated; import org.openrewrite.internal.lang.Nullable; @@ -42,9 +43,11 @@ public class NamedStyles implements Marker { @EqualsAndHashCode.Include String name; + @NlsRewrite.DisplayName String displayName; @Nullable + @NlsRewrite.Description String description; Set tags; diff --git a/rewrite-core/src/main/java/org/openrewrite/text/Find.java b/rewrite-core/src/main/java/org/openrewrite/text/Find.java index 5a631495016..d9270d801d6 100644 --- a/rewrite-core/src/main/java/org/openrewrite/text/Find.java +++ b/rewrite-core/src/main/java/org/openrewrite/text/Find.java @@ -46,7 +46,7 @@ public String getDisplayName() { @Override public String getDescription() { - return "Search for text, treating all textual sources as plain text."; + return "Textual search, optionally using Regular Expression (regex) to query."; } @Option(displayName = "Find", @@ -55,7 +55,7 @@ public String getDescription() { String find; @Option(displayName = "Regex", - description = "If true, `find` will be interpreted as a Regular Expression. Default `false`.", + description = "If true, `find` will be interpreted as a [Regular Expression](https://en.wikipedia.org/wiki/Regular_expression). Default `false`.", required = false) @Nullable Boolean regex; diff --git a/rewrite-core/src/main/java/org/openrewrite/text/FindAndReplace.java b/rewrite-core/src/main/java/org/openrewrite/text/FindAndReplace.java index a24333a9094..f7d7b0f14b0 100644 --- a/rewrite-core/src/main/java/org/openrewrite/text/FindAndReplace.java +++ b/rewrite-core/src/main/java/org/openrewrite/text/FindAndReplace.java @@ -37,6 +37,19 @@ @EqualsAndHashCode(callSuper = false) public class FindAndReplace extends Recipe { + @Override + public String getDisplayName() { + return "Find and replace"; + } + + @Override + public String getDescription() { + return "Textual find and replace, optionally interpreting the search query as a Regular Expression (regex). " + + "When operating on source files that are language-specific Lossless Semantic " + + "Tree, such as Java or XML, this operation converts the source file to plain text for the rest of the recipe run. " + + "So if you are combining this recipe with language-specific recipes in a single recipe run put all the language-specific recipes before this recipe."; + } + @Option(displayName = "Find", description = "The text to find (and replace). This snippet can be multiline.", example = "blacklist") @@ -50,7 +63,7 @@ public class FindAndReplace extends Recipe { String replace; @Option(displayName = "Regex", - description = "Default false. If true, `find` will be interpreted as a Regular Expression, and capture group contents will be available in `replace`.", + description = "Default false. If true, `find` will be interpreted as a [Regular Expression](https://en.wikipedia.org/wiki/Regular_expression), and capture group contents will be available in `replace`.", required = false) @Nullable Boolean regex; @@ -86,18 +99,6 @@ public class FindAndReplace extends Recipe { @Nullable String filePattern; - @Override - public String getDisplayName() { - return "Find and replace"; - } - - @Override - public String getDescription() { - return "Simple text find and replace. When the original source file is a language-specific Lossless Semantic " + - "Tree, this operation irreversibly converts the source file to a plain text file. Subsequent recipes " + - "will not be able to operate on language-specific type."; - } - @Override public TreeVisitor getVisitor() { TreeVisitor visitor = new TreeVisitor() { diff --git a/rewrite-core/src/test/java/org/openrewrite/config/DeclarativeRecipeTest.java b/rewrite-core/src/test/java/org/openrewrite/config/DeclarativeRecipeTest.java index f3dfcabbd33..227eb73eef4 100644 --- a/rewrite-core/src/test/java/org/openrewrite/config/DeclarativeRecipeTest.java +++ b/rewrite-core/src/test/java/org/openrewrite/config/DeclarativeRecipeTest.java @@ -71,6 +71,57 @@ public PlainText visitText(PlainText text, ExecutionContext ctx) { ); } + @Test + void uninitializedFailsValidation() { + DeclarativeRecipe dr = new DeclarativeRecipe("test", "test", "test", null, + null, null, true, null); + dr.addUninitializedPrecondition( + toRecipe(() -> new PlainTextVisitor<>() { + @Override + public PlainText visitText(PlainText text, ExecutionContext ctx) { + if ("1".equals(text.getText())) { + return SearchResult.found(text); + } + return text; + } + }) + ); + dr.addUninitialized( + new ChangeText("2") + ); + dr.addUninitialized( + new ChangeText("3") + ); + Validated validation = dr.validate(); + assertThat(validation.isValid()).isFalse(); + assertThat(validation.failures().size()).isEqualTo(2); + assertThat(validation.failures().get(0).getProperty()).isEqualTo("initialization"); + } + + @Test + void uninitializedWithInitializedRecipesPassesValidation() { + DeclarativeRecipe dr = new DeclarativeRecipe("test", "test", "test", null, + null, null, true, null); + dr.setPreconditions( + List.of( + toRecipe(() -> new PlainTextVisitor<>() { + @Override + public PlainText visitText(PlainText text, ExecutionContext ctx) { + if ("1".equals(text.getText())) { + return SearchResult.found(text); + } + return text; + } + })) + ); + dr.setRecipeList(List.of( + new ChangeText("2"), + new ChangeText("3") + )); + Validated validation = dr.validate(); + assertThat(validation.isValid()).isTrue(); + } + @Test void yamlPrecondition() { rewriteRun( @@ -96,17 +147,17 @@ void yamlPrecondition() { void yamlPreconditionWithScanningRecipe() { rewriteRun( spec -> spec.recipeFromYaml(""" - --- - type: specs.openrewrite.org/v1beta/recipe - name: org.openrewrite.PreconditionTest - preconditions: - - org.openrewrite.text.Find: - find: 1 - recipeList: - - org.openrewrite.text.CreateTextFile: - relativeFileName: test.txt - fileContents: "test" - """, "org.openrewrite.PreconditionTest") + --- + type: specs.openrewrite.org/v1beta/recipe + name: org.openrewrite.PreconditionTest + preconditions: + - org.openrewrite.text.Find: + find: 1 + recipeList: + - org.openrewrite.text.CreateTextFile: + relativeFileName: test.txt + fileContents: "test" + """, "org.openrewrite.PreconditionTest") .afterRecipe(run -> { assertThat(run.getChangeset().getAllResults()).anySatisfy( s -> { diff --git a/rewrite-gradle/src/main/java/org/openrewrite/gradle/ChangeDependencyClassifier.java b/rewrite-gradle/src/main/java/org/openrewrite/gradle/ChangeDependencyClassifier.java index baec86def0d..86be691bc61 100644 --- a/rewrite-gradle/src/main/java/org/openrewrite/gradle/ChangeDependencyClassifier.java +++ b/rewrite-gradle/src/main/java/org/openrewrite/gradle/ChangeDependencyClassifier.java @@ -27,11 +27,12 @@ import org.openrewrite.internal.StringUtils; import org.openrewrite.internal.lang.Nullable; import org.openrewrite.java.MethodMatcher; -import org.openrewrite.java.tree.Expression; -import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.*; +import org.openrewrite.marker.Markers; import org.openrewrite.semver.DependencyMatcher; import java.util.List; +import java.util.Objects; import static java.util.Objects.requireNonNull; @@ -50,7 +51,9 @@ public class ChangeDependencyClassifier extends Recipe { @Option(displayName = "New classifier", description = "A qualification classifier for the dependency.", - example = "sources") + example = "sources", + required = false) + @Nullable String newClassifier; @Option(displayName = "Dependency configuration", @@ -98,7 +101,7 @@ public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) String gav = (String) ((J.Literal) depArgs.get(0)).getValue(); if (gav != null) { Dependency dependency = DependencyStringNotationConverter.parse(gav); - if (dependency != null && dependency.getVersion() != null && dependency.getClassifier() != null && !newClassifier.equals(dependency.getClassifier()) && + if (dependency != null && dependency.getVersion() != null && !Objects.equals(newClassifier, dependency.getClassifier()) && depMatcher.matches(dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion())) { Dependency newDependency = dependency.withClassifier(newClassifier); m = m.withArguments(ListUtils.mapFirst(m.getArguments(), arg -> ChangeStringLiteral.withStringValue((J.Literal) arg, newDependency.toStringNotation()))); @@ -111,7 +114,10 @@ public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) String version = null; String classifier = null; - String classifierStringDelimiter = "'"; + String groupDelimiter = "'"; + G.MapEntry mapEntry = null; + String classifierStringDelimiter = null; + int index = 0; for (Expression e : depArgs) { if (!(e instanceof G.MapEntry)) { continue; @@ -129,38 +135,61 @@ public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) String valueValue = (String) value.getValue(); if ("group".equals(keyValue)) { groupId = valueValue; + if (value.getValueSource() != null) { + groupDelimiter = value.getValueSource().substring(0, value.getValueSource().indexOf(valueValue)); + } } else if ("name".equals(keyValue)) { + if (index > 0 && mapEntry == null) { + mapEntry = arg; + } artifactId = valueValue; } else if ("version".equals(keyValue)) { version = valueValue; - } else if ("classifier".equals(keyValue) && !newClassifier.equals(valueValue)) { + } else if ("classifier".equals(keyValue)) { if (value.getValueSource() != null) { classifierStringDelimiter = value.getValueSource().substring(0, value.getValueSource().indexOf(valueValue)); } classifierEntry = arg; classifier = valueValue; } + index++; } if (groupId == null || artifactId == null || (version == null && !depMatcher.matches(groupId, artifactId)) || (version != null && !depMatcher.matches(groupId, artifactId, version)) - || classifier == null) { + || Objects.equals(newClassifier, classifier)) { return m; } - String delimiter = classifierStringDelimiter; - G.MapEntry finalClassifier = classifierEntry; - m = m.withArguments(ListUtils.map(m.getArguments(), arg -> { - if (arg == finalClassifier) { - return finalClassifier.withValue(((J.Literal) finalClassifier.getValue()) - .withValue(newClassifier) - .withValueSource(delimiter + newClassifier + delimiter)); + + if (classifier == null) { + String delimiter = groupDelimiter; + List args = m.getArguments(); + J.Literal keyLiteral = new J.Literal(Tree.randomId(), mapEntry == null ? Space.EMPTY : mapEntry.getKey().getPrefix(), Markers.EMPTY, "classifier", "classifier", null, JavaType.Primitive.String); + J.Literal valueLiteral = new J.Literal(Tree.randomId(), mapEntry == null ? Space.EMPTY : mapEntry.getValue().getPrefix(), Markers.EMPTY, newClassifier, delimiter + newClassifier + delimiter, null, JavaType.Primitive.String); + args.add(new G.MapEntry(Tree.randomId(), mapEntry == null ? Space.EMPTY : mapEntry.getPrefix(), Markers.EMPTY, JRightPadded.build(keyLiteral), valueLiteral, null)); + m = m.withArguments(args); + } else { + G.MapEntry finalClassifier = classifierEntry; + if (newClassifier == null) { + m = m.withArguments(ListUtils.map(m.getArguments(), arg -> arg == finalClassifier ? null : arg)); + } else { + String delimiter = classifierStringDelimiter; // `classifierStringDelimiter` cannot be null + m = m.withArguments(ListUtils.map(m.getArguments(), arg -> { + if (arg == finalClassifier) { + return finalClassifier.withValue(((J.Literal) finalClassifier.getValue()) + .withValue(newClassifier) + .withValueSource(delimiter + newClassifier + delimiter)); + } + return arg; + })); } - return arg; - })); + } + } return m; } }); } + } diff --git a/rewrite-gradle/src/test/java/org/openrewrite/gradle/ChangeDependencyClassifierTest.java b/rewrite-gradle/src/test/java/org/openrewrite/gradle/ChangeDependencyClassifierTest.java index ddb1922e5fb..caad9c5bccb 100644 --- a/rewrite-gradle/src/test/java/org/openrewrite/gradle/ChangeDependencyClassifierTest.java +++ b/rewrite-gradle/src/test/java/org/openrewrite/gradle/ChangeDependencyClassifierTest.java @@ -34,7 +34,7 @@ void worksWithEmptyStringConfig() { plugins { id 'java-library' } - + repositories { mavenCentral() } @@ -47,7 +47,7 @@ void worksWithEmptyStringConfig() { plugins { id 'java-library' } - + repositories { mavenCentral() } @@ -108,7 +108,7 @@ void findMapStyleDependency(String group, String artifact) { plugins { id 'java-library' } - + repositories { mavenCentral() } @@ -122,7 +122,7 @@ void findMapStyleDependency(String group, String artifact) { plugins { id 'java-library' } - + repositories { mavenCentral() } @@ -146,7 +146,7 @@ void worksWithoutVersion(String group, String artifact) { plugins { id 'java-library' } - + repositories { mavenCentral() } @@ -160,7 +160,7 @@ void worksWithoutVersion(String group, String artifact) { plugins { id 'java-library' } - + repositories { mavenCentral() } @@ -184,7 +184,7 @@ void worksWithClassifier(String group, String artifact) { plugins { id 'java-library' } - + repositories { mavenCentral() } @@ -200,7 +200,7 @@ void worksWithClassifier(String group, String artifact) { plugins { id 'java-library' } - + repositories { mavenCentral() } @@ -226,7 +226,7 @@ void worksWithExt(String group, String artifact) { plugins { id 'java-library' } - + repositories { mavenCentral() } @@ -246,7 +246,7 @@ void worksWithExt(String group, String artifact) { plugins { id 'java-library' } - + repositories { mavenCentral() } @@ -265,4 +265,150 @@ void worksWithExt(String group, String artifact) { ) ); } + + @Test + void noPreviousClassifier_1() { + rewriteRun( + spec -> spec.recipe(new ChangeDependencyClassifier("org.openrewrite", "*", "classified", "")), + buildGradle( + """ + plugins { + id 'java-library' + } + + repositories { + mavenCentral() + } + + dependencies { + api 'org.openrewrite:rewrite-gradle:latest.release' + } + """, + """ + plugins { + id 'java-library' + } + + repositories { + mavenCentral() + } + + dependencies { + api 'org.openrewrite:rewrite-gradle:latest.release:classified' + } + """ + ) + ); + } + + @ParameterizedTest + @CsvSource(value = {"org.openrewrite:rewrite-core", "*:*"}, delimiterString = ":") + void noPreviousClassifier_2(String group, String artifact) { + rewriteRun( + spec -> spec.recipe(new ChangeDependencyClassifier(group, artifact, "classified", null)), + buildGradle( + """ + plugins { + id 'java-library' + } + + repositories { + mavenCentral() + } + + dependencies { + api(group: 'org.openrewrite', name: 'rewrite-core', version: 'latest.release') + api(group: "org.openrewrite", name: "rewrite-core", version: "latest.release") + } + """, + """ + plugins { + id 'java-library' + } + + repositories { + mavenCentral() + } + + dependencies { + api(group: 'org.openrewrite', name: 'rewrite-core', version: 'latest.release', classifier: 'classified') + api(group: "org.openrewrite", name: "rewrite-core", version: "latest.release", classifier: "classified") + } + """ + ) + ); + } + + @Test + void noNewClassifier_1() { + rewriteRun( + spec -> spec.recipe(new ChangeDependencyClassifier("org.openrewrite", "*", null, "")), + buildGradle( + """ + plugins { + id 'java-library' + } + + repositories { + mavenCentral() + } + + dependencies { + api 'org.openrewrite:rewrite-gradle:latest.release:classified' + } + """, + """ + plugins { + id 'java-library' + } + + repositories { + mavenCentral() + } + + dependencies { + api 'org.openrewrite:rewrite-gradle:latest.release' + } + """ + ) + ); + } + + @ParameterizedTest + @CsvSource(value = {"org.openrewrite:rewrite-core", "*:*"}, delimiterString = ":") + void noNewClassifier_2(String group, String artifact) { + rewriteRun( + spec -> spec.recipe(new ChangeDependencyClassifier(group, artifact, null, null)), + buildGradle( + """ + plugins { + id 'java-library' + } + + repositories { + mavenCentral() + } + + dependencies { + api(group: 'org.openrewrite', name: 'rewrite-core', version: 'latest.release', classifier: 'classified') + api(group: "org.openrewrite", name: "rewrite-core", version: "latest.release", classifier: "classified") + } + """, + """ + plugins { + id 'java-library' + } + + repositories { + mavenCentral() + } + + dependencies { + api(group: 'org.openrewrite', name: 'rewrite-core', version: 'latest.release') + api(group: "org.openrewrite", name: "rewrite-core", version: "latest.release") + } + """ + ) + ); + } } diff --git a/rewrite-groovy/src/main/java/org/openrewrite/groovy/GroovyPrinter.java b/rewrite-groovy/src/main/java/org/openrewrite/groovy/GroovyPrinter.java index 313ae563178..5bea1f9109c 100644 --- a/rewrite-groovy/src/main/java/org/openrewrite/groovy/GroovyPrinter.java +++ b/rewrite-groovy/src/main/java/org/openrewrite/groovy/GroovyPrinter.java @@ -397,12 +397,9 @@ public J visitReturn(J.Return return_, PrintOutputCapture

p) { return super.visitReturn(return_, p); } - protected void visitStatement(@Nullable JRightPadded paddedStat, JRightPadded.Location location, PrintOutputCapture

p) { - if (paddedStat != null) { - visit(paddedStat.getElement(), p); - visitSpace(paddedStat.getAfter(), location.getAfterLocation(), p); - visitMarkers(paddedStat.getMarkers(), p); - } + @Override + protected void printStatementTerminator(Statement s, PrintOutputCapture

p) { + // empty } @Override diff --git a/rewrite-java-test/src/test/java/org/openrewrite/java/JavaTemplateSubstitutionsTest.java b/rewrite-java-test/src/test/java/org/openrewrite/java/JavaTemplateSubstitutionsTest.java index fd191e47546..24ad9c9ff3f 100644 --- a/rewrite-java-test/src/test/java/org/openrewrite/java/JavaTemplateSubstitutionsTest.java +++ b/rewrite-java-test/src/test/java/org/openrewrite/java/JavaTemplateSubstitutionsTest.java @@ -141,7 +141,7 @@ class Test { """, """ class Test { - + @SuppressWarnings("ALL") void test2() { } @@ -412,10 +412,10 @@ void test(boolean condition) { """, """ import java.util.Arrays; - + abstract class Test { abstract String[] array(); - + void test(boolean condition) { Object any = Arrays.asList(condition ? array() : new String[]{"Hello!"}); } @@ -457,4 +457,34 @@ void test(Map map) { ) ); } + + @Test + void throwNewException() { + rewriteRun( + spec -> spec.recipe(toRecipe(() -> new JavaVisitor<>() { + @Override + public J visitMethodInvocation(J.MethodInvocation methodInvocation, ExecutionContext executionContext) { + return JavaTemplate.builder("throw new RuntimeException()") + .build() + .apply(getCursor(), methodInvocation.getCoordinates().replace()); + } + })), + java( + """ + public class Test { + void test() { + System.out.println("Hello"); + } + } + """, + """ + public class Test { + void test() { + throw new RuntimeException(); + } + } + """ + ) + ); + } } diff --git a/rewrite-java-test/src/test/java/org/openrewrite/java/search/SemanticallyEqualTest.java b/rewrite-java-test/src/test/java/org/openrewrite/java/search/SemanticallyEqualTest.java index 7be41ac53ab..722d4725cfe 100644 --- a/rewrite-java-test/src/test/java/org/openrewrite/java/search/SemanticallyEqualTest.java +++ b/rewrite-java-test/src/test/java/org/openrewrite/java/search/SemanticallyEqualTest.java @@ -17,6 +17,7 @@ import org.intellij.lang.annotations.Language; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Nested; import org.junitpioneer.jupiter.ExpectedToFail; import org.junitpioneer.jupiter.cartesian.CartesianTest; import org.openrewrite.java.JavaIsoVisitor; @@ -298,6 +299,74 @@ class T { ); } + @Nested + class Generics { + @Test + void noneEmpty() { + assertExpressionsEqual( + """ + import java.util.List; + class T { + List a = new java.util.ArrayList(); + List b = new java.util.ArrayList(); + } + """ + ); + } + + @Test + void firstEmpty() { + assertExpressionsEqual( + """ + import java.util.List; + class T { + List a = new java.util.ArrayList(); + List b = new java.util.ArrayList<>(); + } + """ + ); + } + + @Test + void secondEmpty() { + assertExpressionsEqual( + """ + import java.util.List; + class T { + List a = new java.util.ArrayList<>(); + List b = new java.util.ArrayList(); + } + """ + ); + } + + @Test + void bothEmpty() { + assertExpressionsEqual( + """ + import java.util.List; + class T { + List a = new java.util.ArrayList<>(); + List b = new java.util.ArrayList<>(); + } + """ + ); + } + + @Test + void bothEmptyButDifferent() { + assertExpressionsNotEqual( + """ + import java.util.List; + class T { + List a = new java.util.ArrayList<>(); + List b = new java.util.ArrayList<>(); + } + """ + ); + } + } + private void assertEqualToSelf(@Language("java") String a) { assertEqual(a, a); } diff --git a/rewrite-java/src/main/java/org/openrewrite/java/JavaPrinter.java b/rewrite-java/src/main/java/org/openrewrite/java/JavaPrinter.java index 03f677d1bcc..a8ec988c14a 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/JavaPrinter.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/JavaPrinter.java @@ -138,6 +138,18 @@ protected void visitModifier(Modifier mod, PrintOutputCapture

p) { case Volatile: keyword = "volatile"; break; + case Async: + keyword = "async"; + break; + case Reified: + keyword = "reified"; + break; + case Inline: + keyword = "inline"; + break; + case LanguageExtension: + keyword = mod.getKeyword(); + break; } beforeSyntax(mod, Space.Location.MODIFIER_PREFIX, p); p.append(keyword); @@ -375,8 +387,11 @@ protected void visitStatement(@Nullable JRightPadded paddedStat, JRig visit(paddedStat.getElement(), p); visitSpace(paddedStat.getAfter(), location.getAfterLocation(), p); + visitMarkers(paddedStat.getMarkers(), p); + printStatementTerminator(paddedStat.getElement(), p); + } - Statement s = paddedStat.getElement(); + protected void printStatementTerminator(Statement s, PrintOutputCapture

p) { while (true) { if (s instanceof Assert || s instanceof Assignment || @@ -415,9 +430,9 @@ protected void visitStatement(@Nullable JRightPadded paddedStat, JRig c == Cursor.ROOT_VALUE ) .getValue(); - if (aSwitch instanceof J.SwitchExpression) { + if (aSwitch instanceof SwitchExpression) { Case aCase = getCursor().getValue(); - if (!(aCase.getBody() instanceof J.Block)) { + if (!(aCase.getBody() instanceof Block)) { p.append(';'); } return; diff --git a/rewrite-java/src/main/java/org/openrewrite/java/internal/template/BlockStatementTemplateGenerator.java b/rewrite-java/src/main/java/org/openrewrite/java/internal/template/BlockStatementTemplateGenerator.java index 7d0f9f04866..5a8644abb80 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/internal/template/BlockStatementTemplateGenerator.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/internal/template/BlockStatementTemplateGenerator.java @@ -212,11 +212,17 @@ protected void contextFreeTemplate(Cursor cursor, J j, StringBuilder before, Str throw new IllegalArgumentException( "Templating a method reference requires a cursor so that it can be properly parsed and type-attributed. " + "Mark this template as context-sensitive by calling JavaTemplate.Builder#contextSensitive()."); + } else if (j instanceof J.MethodInvocation) { + before.insert(0, "class Template {{\n"); + JavaType.Method methodType = ((J.MethodInvocation) j).getMethodType(); + if (methodType == null || methodType.getReturnType() != JavaType.Primitive.Void) { + before.append("Object o = "); + } + after.append(";\n}}"); } else if (j instanceof Expression && !(j instanceof J.Assignment)) { before.insert(0, "class Template {\n"); before.append("Object o = "); - after.append(";"); - after.append("\n}"); + after.append(";\n}"); } else if ((j instanceof J.MethodDeclaration || j instanceof J.VariableDeclarations || j instanceof J.Block || j instanceof J.ClassDeclaration) && cursor.getValue() instanceof J.Block && (cursor.getParent().getValue() instanceof J.ClassDeclaration || cursor.getParent().getValue() instanceof J.NewClass)) { diff --git a/rewrite-java/src/main/java/org/openrewrite/java/internal/template/Substitutions.java b/rewrite-java/src/main/java/org/openrewrite/java/internal/template/Substitutions.java index bd08f60573e..1afdf93dce2 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/internal/template/Substitutions.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/internal/template/Substitutions.java @@ -16,6 +16,7 @@ package org.openrewrite.java.internal.template; import lombok.RequiredArgsConstructor; +import lombok.ToString; import org.antlr.v4.runtime.*; import org.openrewrite.internal.ListUtils; import org.openrewrite.internal.PropertyPlaceholderHelper; @@ -34,6 +35,7 @@ import java.util.regex.Pattern; @RequiredArgsConstructor +@ToString public class Substitutions { private static final Pattern PATTERN_COMMENT = Pattern.compile("__p(\\d+)__"); diff --git a/rewrite-java/src/main/java/org/openrewrite/java/search/SemanticallyEqual.java b/rewrite-java/src/main/java/org/openrewrite/java/search/SemanticallyEqual.java index b7221e049a4..1fa70ed3fee 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/search/SemanticallyEqual.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/search/SemanticallyEqual.java @@ -1088,7 +1088,9 @@ public J.ParameterizedType visitParameterizedType(J.ParameterizedType type, J j) return type; } - this.visitList(type.getTypeParameters(), compareTo.getTypeParameters()); + if (!(type.getTypeParameters().get(0) instanceof J.Empty || compareTo.getTypeParameters().get(0) instanceof J.Empty)) { + this.visitList(type.getTypeParameters(), compareTo.getTypeParameters()); + } } return type; } diff --git a/rewrite-java/src/main/java/org/openrewrite/java/tree/JavaType.java b/rewrite-java/src/main/java/org/openrewrite/java/tree/JavaType.java index 4355021d5ba..c3045457ce1 100644 --- a/rewrite-java/src/main/java/org/openrewrite/java/tree/JavaType.java +++ b/rewrite-java/src/main/java/org/openrewrite/java/tree/JavaType.java @@ -715,7 +715,12 @@ public String getFullyQualifiedName() { @Override public FullyQualified withFullyQualifiedName(String fullyQualifiedName) { - return type.withFullyQualifiedName(fullyQualifiedName); + FullyQualified qualified = type.withFullyQualifiedName(fullyQualifiedName); + if (type == qualified) { + return this; + } + + return new Parameterized(managedReference, qualified, typeParameters); } @Override diff --git a/rewrite-java/src/test/java/org/openrewrite/java/internal/template/TypeParameterTest.java b/rewrite-java/src/test/java/org/openrewrite/java/internal/template/TypeParameterTest.java index b74b0f4a07b..a909ba47eeb 100644 --- a/rewrite-java/src/test/java/org/openrewrite/java/internal/template/TypeParameterTest.java +++ b/rewrite-java/src/test/java/org/openrewrite/java/internal/template/TypeParameterTest.java @@ -89,6 +89,25 @@ void parameterized(String name) { assertThat(TypeUtils.toString(type)).isEqualTo(name); } + @ParameterizedTest + @ValueSource(strings = { + "java.util.List", + "java.util.Map", + "java.util.List", + "java.util.List", + "java.util.List>", + }) + void parameterizedWithModifierShouldNeverHideParametrizedType(String name) { + TemplateParameterParser parser = new TemplateParameterParser(new CommonTokenStream(new TemplateParameterLexer( + CharStreams.fromString(name)))); + JavaType type = TypeParameter.toFullyQualifiedName(parser.type()); + + JavaType.Parameterized pType = (JavaType.Parameterized) type; + assertThat(pType.withFullyQualifiedName("test")).isInstanceOf(JavaType.Parameterized.class); + assertThat(pType.withFullyQualifiedName("test")).isNotSameAs(pType); + assertThat(pType.withFullyQualifiedName(pType.getFullyQualifiedName())).isSameAs(pType); + } + @ParameterizedTest @ValueSource(strings = { "java.util.List", diff --git a/rewrite-java/src/test/java/org/openrewrite/java/recipes/BlankLinesAroundFieldsWithAnnotationsTest.java b/rewrite-java/src/test/java/org/openrewrite/java/recipes/BlankLinesAroundFieldsWithAnnotationsTest.java index 8f406717444..e623263487d 100644 --- a/rewrite-java/src/test/java/org/openrewrite/java/recipes/BlankLinesAroundFieldsWithAnnotationsTest.java +++ b/rewrite-java/src/test/java/org/openrewrite/java/recipes/BlankLinesAroundFieldsWithAnnotationsTest.java @@ -16,12 +16,13 @@ package org.openrewrite.java.recipes; import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; import org.openrewrite.test.RecipeSpec; import org.openrewrite.test.RewriteTest; import static org.openrewrite.java.Assertions.java; -public class BlankLinesAroundFieldsWithAnnotationsTest implements RewriteTest { +class BlankLinesAroundFieldsWithAnnotationsTest implements RewriteTest { @Override public void defaults(RecipeSpec spec) { @@ -29,6 +30,7 @@ public void defaults(RecipeSpec spec) { } @Test + @DocumentExample void spaceBetweenFields() { rewriteRun( java( diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/AddRepository.java b/rewrite-maven/src/main/java/org/openrewrite/maven/AddRepository.java index 79a4e29ac42..92a38ec2283 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/AddRepository.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/AddRepository.java @@ -16,6 +16,7 @@ package org.openrewrite.maven; import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; import lombok.Value; import org.intellij.lang.annotations.Language; import org.openrewrite.ExecutionContext; @@ -35,7 +36,6 @@ @Value @EqualsAndHashCode(callSuper = false) public class AddRepository extends Recipe { - private static final XPathMatcher REPOS_MATCHER = new XPathMatcher("/project/repositories"); @Option(example = "repo-id", displayName = "Repository ID", description = "A unique name to describe the repository.") @@ -93,6 +93,26 @@ public class AddRepository extends Recipe { @Nullable String releasesUpdatePolicy; + @Option(displayName = "Repository type", + description = "The type of repository to add.", + example = "Repository", + required = false) + @Nullable + Type type; + + @RequiredArgsConstructor + public enum Type { + Repository("repository", "repositories"), + PluginRepository("pluginRepository", "pluginRepositories"); + + final String xmlTagSingle; + final String xmlTagPlural; + } + + public Type getType() { + return type == null ? Type.Repository : type; + } + @Override public String getDisplayName() { return "Add repository"; @@ -106,11 +126,13 @@ public String getDescription() { @Override public TreeVisitor getVisitor() { return new MavenIsoVisitor() { + private final XPathMatcher REPOS_MATCHER = new XPathMatcher("/project/" + getType().xmlTagPlural); + @Override public Xml.Document visitDocument(Xml.Document document, ExecutionContext ctx) { Xml.Tag root = document.getRoot(); - if (!root.getChild("repositories").isPresent()) { - document = (Xml.Document) new AddToTagVisitor<>(root, Xml.Tag.build("")) + if (!root.getChild(getType().xmlTagPlural).isPresent()) { + document = (Xml.Document) new AddToTagVisitor<>(root, Xml.Tag.build("<" + getType().xmlTagPlural + "/>")) .visitNonNull(document, ctx, getCursor().getParentOrThrow()); } return super.visitDocument(document, ctx); @@ -123,7 +145,7 @@ public Xml.Tag visitTag(Xml.Tag tag, ExecutionContext ctx) { if (REPOS_MATCHER.matches(getCursor())) { Optional maybeRepo = repositories.getChildren().stream() .filter(repo -> - "repository".equals(repo.getName()) && + getType().xmlTagSingle.equals(repo.getName()) && (id.equals(repo.getChildValue("id").orElse(null)) || (isReleasesEqual(repo) && isSnapshotsEqual(repo))) && url.equals(repo.getChildValue("url").orElse(null)) ) @@ -171,14 +193,14 @@ public Xml.Tag visitTag(Xml.Tag tag, ExecutionContext ctx) { } } else { @Language("xml") - String sb = "\n" + + String sb = "<" + getType().xmlTagSingle + ">\n" + assembleTagWithValue("id", id) + assembleTagWithValue("url", url) + assembleTagWithValue("name", repoName) + assembleTagWithValue("layout", layout) + assembleReleases() + assembleSnapshots() + - "\n"; + "\n"; Xml.Tag repoTag = Xml.Tag.build(sb); repositories = (Xml.Tag) new AddToTagVisitor<>(repositories, repoTag).visitNonNull(repositories, ctx, getCursor().getParentOrThrow()); diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/MavenExecutionContextView.java b/rewrite-maven/src/main/java/org/openrewrite/maven/MavenExecutionContextView.java index e8a86a3c9c8..ef1760dafa8 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/MavenExecutionContextView.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/MavenExecutionContextView.java @@ -256,24 +256,44 @@ private static List mapCredentials(MavenSettings set private static List mapMirrors(MavenSettings settings) { if (settings.getMirrors() != null) { return settings.getMirrors().getMirrors().stream() - .map(mirror -> new MavenRepositoryMirror(mirror.getId(), mirror.getUrl(), mirror.getMirrorOf(), mirror.getReleases(), mirror.getSnapshots())) + .map(mirror -> new MavenRepositoryMirror(mirror.getId(), mirror.getUrl(), mirror.getMirrorOf(), mirror.getReleases(), mirror.getSnapshots(), settings.getServers())) .collect(Collectors.toList()); } return emptyList(); } private List mapRepositories(MavenSettings settings, List activeProfiles) { + Map repositories = this.getRepositories().stream() + .collect(Collectors.toMap(MavenRepository::getId, r -> r, (a, b) -> a)); return settings.getActiveRepositories(activeProfiles).stream() .map(repo -> { try { - return new MavenRepository( - repo.getId(), - repo.getUrl(), - repo.getReleases() == null ? null : repo.getReleases().getEnabled(), - repo.getSnapshots() == null ? null : repo.getSnapshots().getEnabled(), - null, - null - ); + MavenRepository knownRepo = repositories.get(repo.getId()); + if (knownRepo != null) { + return new MavenRepository( + repo.getId(), + repo.getUrl(), + repo.getReleases() != null ? repo.getReleases().getEnabled() : knownRepo.getReleases(), + repo.getSnapshots() != null ? repo.getSnapshots().getEnabled() : knownRepo.getSnapshots(), + knownRepo.isKnownToExist() && knownRepo.getUri().equals(repo.getUrl()), + knownRepo.getUsername(), + knownRepo.getPassword(), + knownRepo.getConnectTimeout(), + knownRepo.getReadTimeout(), + knownRepo.getDeriveMetadataIfMissing() + ); + } else { + return new MavenRepository( + repo.getId(), + repo.getUrl(), + repo.getReleases() == null ? null : repo.getReleases().getEnabled(), + repo.getSnapshots() == null ? null : repo.getSnapshots().getEnabled(), + null, + null, + null, + null + ); + } } catch (Exception exception) { this.getOnError().accept(new MavenParsingException( "Unable to parse URL %s for Maven settings repository id %s", diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/MavenSettings.java b/rewrite-maven/src/main/java/org/openrewrite/maven/MavenSettings.java index 23fd7302874..6114e437fb0 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/MavenSettings.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/MavenSettings.java @@ -16,8 +16,11 @@ package org.openrewrite.maven; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; import lombok.*; import lombok.experimental.FieldDefaults; import lombok.experimental.NonFinal; @@ -46,12 +49,14 @@ @EqualsAndHashCode(onlyExplicitlyIncluded = true) @Data @AllArgsConstructor +@JacksonXmlRootElement(localName = "settings") public class MavenSettings { @Nullable String localRepository; @Nullable @NonFinal + @JsonIgnore MavenRepository mavenLocal; @Nullable @@ -238,8 +243,11 @@ private ServerConfiguration interpolate(@Nullable ServerConfiguration configurat if (configuration == null) { return null; } - return new ServerConfiguration(configuration.httpHeaders == null ? null : - ListUtils.map(configuration.httpHeaders, this::interpolate)); + return new ServerConfiguration( + ListUtils.map(configuration.httpHeaders, this::interpolate), + configuration.connectTimeout, + configuration.readTimeout + ); } private HttpHeader interpolate(HttpHeader httpHeader) { @@ -403,12 +411,27 @@ public static class Server { ServerConfiguration configuration; } + @SuppressWarnings("DefaultAnnotationParam") @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) @Data @With + @JsonIgnoreProperties(value = "httpHeaders") public static class ServerConfiguration { + @JacksonXmlProperty(localName = "property") + @JacksonXmlElementWrapper(localName = "httpHeaders", useWrapping = true) // wrapping is disabled by default on MavenXmlMapper @Nullable List httpHeaders; + /** + * Timeout in milliseconds for establishing a connection. + */ + @Nullable + Long connectTimeout; + + /** + * Timeout in milliseconds for reading from the connection. + */ + @Nullable + Long readTimeout; } @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/UpdateMavenModel.java b/rewrite-maven/src/main/java/org/openrewrite/maven/UpdateMavenModel.java index a0ba344772f..58de657afd6 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/UpdateMavenModel.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/UpdateMavenModel.java @@ -119,6 +119,8 @@ public Xml visitDocument(Xml.Document document, P p) { t.getChild("releases").flatMap(s -> s.getChildValue("enabled")).orElse(null), t.getChild("snapshots").flatMap(s -> s.getChildValue("enabled")).orElse(null), null, + null, + null, null )).collect(Collectors.toList())); } else { diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/internal/MavenPomDownloader.java b/rewrite-maven/src/main/java/org/openrewrite/maven/internal/MavenPomDownloader.java index b65a1fb214d..f23a68d460c 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/internal/MavenPomDownloader.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/internal/MavenPomDownloader.java @@ -98,7 +98,7 @@ public MavenPomDownloader(Map projectPoms, ExecutionContext ctx, @Nullable MavenSettings mavenSettings, @Nullable List activeProfiles) { this(projectPoms, HttpSenderExecutionContextView.view(ctx).getHttpSender(), ctx); - this.mavenSettings = mavenSettings; + this.mavenSettings = mavenSettings != null ? mavenSettings : MavenExecutionContextView.view(ctx).getSettings(); this.mirrors = this.ctx.getMirrors(mavenSettings); this.activeProfiles = activeProfiles; } @@ -137,6 +137,7 @@ public MavenPomDownloader(Map projectPoms, HttpSender httpSender, Exe this.projectPomsByGav = projectPomsByGav(projectPoms); this.httpSender = httpSender; this.ctx = MavenExecutionContextView.view(ctx); + this.mavenSettings = this.ctx.getSettings(); this.mavenCache = this.ctx.getPomCache(); this.addCentralRepository = !Boolean.FALSE.equals(MavenExecutionContextView.view(ctx).getAddCentralRepository()); this.addLocalRepository = !Boolean.FALSE.equals(MavenExecutionContextView.view(ctx).getAddLocalRepository()); @@ -773,7 +774,9 @@ public MavenRepository normalizeRepository(MavenRepository originalRepository, M repository.getReleases(), repository.getSnapshots(), repository.getUsername(), - repository.getPassword()); + repository.getPassword(), + repository.getConnectTimeout(), + repository.getReadTimeout()); } catch (HttpSenderResponseException e) { //Response was returned from the server, but it was not a 200 OK. The server therefore exists. if (e.isServerReached()) { @@ -783,7 +786,9 @@ public MavenRepository normalizeRepository(MavenRepository originalRepository, M repository.getReleases(), repository.getSnapshots(), repository.getUsername(), - repository.getPassword()); + repository.getPassword(), + repository.getConnectTimeout(), + repository.getReadTimeout()); } } catch (Throwable e) { // ok to fall through here and cache a null @@ -812,7 +817,10 @@ public MavenRepository normalizeRepository(MavenRepository originalRepository, M */ private byte[] requestAsAuthenticatedOrAnonymous(MavenRepository repo, String uriString) throws HttpSenderResponseException, IOException { try { - return sendRequest(applyAuthenticationToRequest(repo, httpSender.get(uriString)).build()); + HttpSender.Request.Builder request = httpSender.get(uriString) + .withConnectTimeout(repo.getConnectTimeout()) + .withReadTimeout(repo.getReadTimeout()); + return sendRequest(applyAuthenticationToRequest(repo, request).build()); } catch (HttpSenderResponseException e) { if (hasCredentials(repo) && e.isClientSideException()) { return retryRequestAnonymously(uriString, e); @@ -845,11 +853,20 @@ private MavenRepository applyAuthenticationToRepository(MavenRepository reposito * Returns a request builder with Authorization header set if the provided repository specifies credentials */ private HttpSender.Request.Builder applyAuthenticationToRequest(MavenRepository repository, HttpSender.Request.Builder request) { - if (ctx.getSettings() != null && ctx.getSettings().getServers() != null) { - for (MavenSettings.Server server : ctx.getSettings().getServers().getServers()) { - if (server.getId().equals(repository.getId()) && server.getConfiguration() != null && server.getConfiguration().getHttpHeaders() != null) { - for (MavenSettings.HttpHeader header : server.getConfiguration().getHttpHeaders()) { - request.withHeader(header.getName(), header.getValue()); + if (mavenSettings != null && mavenSettings.getServers() != null) { + for (MavenSettings.Server server : mavenSettings.getServers().getServers()) { + if (server.getId().equals(repository.getId()) && server.getConfiguration() != null) { + MavenSettings.ServerConfiguration configuration = server.getConfiguration(); + if (server.getConfiguration().getHttpHeaders() != null) { + for (MavenSettings.HttpHeader header : configuration.getHttpHeaders()) { + request.withHeader(header.getName(), header.getValue()); + } + } + if (configuration.getConnectTimeout() != null) { + request.withConnectTimeout(Duration.ofMillis(configuration.getConnectTimeout())); + } + if (configuration.getReadTimeout() != null) { + request.withReadTimeout(Duration.ofMillis(configuration.getReadTimeout())); } } } diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/internal/RawPom.java b/rewrite-maven/src/main/java/org/openrewrite/maven/internal/RawPom.java index 6569a6c1fc4..0e7dfba40af 100755 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/internal/RawPom.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/internal/RawPom.java @@ -439,7 +439,7 @@ private List mapRepositories(@Nullable RawRepositories rawRepos pomRepositories.add(new MavenRepository(r.getId(), r.getUrl(), r.getReleases() == null ? null : r.getReleases().getEnabled(), r.getSnapshots() == null ? null : r.getSnapshots().getEnabled(), - false, null, null, null)); + false, null, null, null, null, null)); } } diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/tree/MavenRepository.java b/rewrite-maven/src/main/java/org/openrewrite/maven/tree/MavenRepository.java index 4dd1f7200c5..6264efa2de5 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/tree/MavenRepository.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/tree/MavenRepository.java @@ -29,7 +29,9 @@ import java.io.Serializable; import java.net.URI; import java.nio.file.Paths; +import java.time.Duration; +@SuppressWarnings("JavadocReference") @JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class, property = "@ref") @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) @EqualsAndHashCode(onlyExplicitlyIncluded = true) @@ -37,9 +39,9 @@ @RequiredArgsConstructor public class MavenRepository implements Serializable { - public static final MavenRepository MAVEN_LOCAL_USER_NEUTRAL = new MavenRepository("local", new File("~/.m2/repository").toString(), "true", "true", true, null, null, false); - public static final MavenRepository MAVEN_LOCAL_DEFAULT = new MavenRepository("local", Paths.get(System.getProperty("user.home"), ".m2", "repository").toUri().toString(), "true", "true", true, null, null, false); - public static final MavenRepository MAVEN_CENTRAL = new MavenRepository("central", "https://repo.maven.apache.org/maven2", "true", "false", true, null, null, true); + public static final MavenRepository MAVEN_LOCAL_USER_NEUTRAL = new MavenRepository("local", new File("~/.m2/repository").toString(), "true", "true", true, null, null, null, null, false); + public static final MavenRepository MAVEN_LOCAL_DEFAULT = new MavenRepository("local", Paths.get(System.getProperty("user.home"), ".m2", "repository").toUri().toString(), "true", "true", true, null, null, null, null, false); + public static final MavenRepository MAVEN_CENTRAL = new MavenRepository("central", "https://repo.maven.apache.org/maven2", "true", "false", true, null, null, null, null, true); @EqualsAndHashCode.Include @With @@ -79,14 +81,51 @@ public class MavenRepository implements Serializable { @Nullable String password; + @With + @Nullable + Duration connectTimeout; + + @With + @Nullable + Duration readTimeout; + @Nullable @NonFinal Boolean deriveMetadataIfMissing; + /** + * Constructor required by {@link org.openrewrite.maven.tree.OpenRewriteModelSerializableTest}. + * + * @deprecated Use {@link #MavenRepository(String, String, String, String, boolean, String, String, Duration, Duration, Boolean)} + */ + @Deprecated + @JsonIgnore + public MavenRepository( + @Nullable String id, String uri, @Nullable String releases, @Nullable String snapshots, + @Nullable String username, @Nullable String password + ) { + this(id, uri, releases, snapshots, false, username, password, null, null, null); + } + + /** + * Constructor required by {@link org.openrewrite.gradle.marker.GradleProject}. + * + * @deprecated Use {@link #MavenRepository(String, String, String, String, boolean, String, String, Duration, Duration, Boolean)} + */ + @Deprecated @JsonIgnore public MavenRepository( @Nullable String id, String uri, @Nullable String releases, @Nullable String snapshots, boolean knownToExist, @Nullable String username, @Nullable String password, @Nullable Boolean deriveMetadataIfMissing + ) { + this(id, uri, releases, snapshots, knownToExist, username, password, null, null, deriveMetadataIfMissing); + } + + @JsonIgnore + public MavenRepository( + @Nullable String id, String uri, @Nullable String releases, @Nullable String snapshots, boolean knownToExist, + @Nullable String username, @Nullable String password, @Nullable Duration connectTimeout, + @Nullable Duration readTimeout, @Nullable Boolean deriveMetadataIfMissing ) { this.id = id; this.uri = uri; @@ -95,6 +134,8 @@ public MavenRepository( this.knownToExist = knownToExist; this.username = username; this.password = password; + this.connectTimeout = connectTimeout; + this.readTimeout = readTimeout; this.deriveMetadataIfMissing = deriveMetadataIfMissing; } @@ -102,6 +143,7 @@ public static Builder builder() { return new Builder(); } + @SuppressWarnings("unused") @Data @FieldDefaults(level = AccessLevel.PRIVATE) @Accessors(fluent = true, chain = true) @@ -115,12 +157,14 @@ public static class Builder { String username; String password; Boolean deriveMetadataIfMissing; + Duration connectTimeout; + Duration readTimeout; private Builder() { } public MavenRepository build() { - return new MavenRepository(id, uri, releases, snapshots, knownToExist, username, password, deriveMetadataIfMissing); + return new MavenRepository(id, uri, releases, snapshots, knownToExist, username, password, connectTimeout, readTimeout, deriveMetadataIfMissing); } public Builder releases(boolean releases) { @@ -163,6 +207,16 @@ public Builder password(String password) { return this; } + public Builder connectTimeout(Duration connectTimeout) { + this.connectTimeout = connectTimeout; + return this; + } + + public Builder readTimeout(Duration readTimeout) { + this.readTimeout = readTimeout; + return this; + } + @Nullable private static String resolveEnvironmentProperty(@Nullable String rawProperty) { if (rawProperty == null) { diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/tree/MavenRepositoryMirror.java b/rewrite-maven/src/main/java/org/openrewrite/maven/tree/MavenRepositoryMirror.java index 64c1843a39f..b801513dcd2 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/tree/MavenRepositoryMirror.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/tree/MavenRepositoryMirror.java @@ -19,6 +19,7 @@ import lombok.Data; import lombok.experimental.FieldDefaults; import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.maven.MavenSettings; import java.net.URI; import java.net.URISyntaxException; @@ -53,12 +54,19 @@ public class MavenRepositoryMirror { @Nullable Boolean snapshots; + @Nullable + Long connectTimeout; + + @Nullable + Long readTimeout; + private final boolean externalOnly; private final List mirrorsOf; private final Set excludedRepos; private final Set includedRepos; - public MavenRepositoryMirror(@Nullable String id, @Nullable String url, @Nullable String mirrorOf, @Nullable Boolean releases, @Nullable Boolean snapshots) { + public MavenRepositoryMirror(@Nullable String id, @Nullable String url, @Nullable String mirrorOf, + @Nullable Boolean releases, @Nullable Boolean snapshots, @Nullable MavenSettings.Servers servers) { this.id = id; this.url = url; this.mirrorOf = mirrorOf; @@ -85,11 +93,30 @@ public MavenRepositoryMirror(@Nullable String id, @Nullable String url, @Nullabl includedRepos.add(mirror); } } + + if (id != null && servers != null && servers.getServers() != null) { + Optional maybeServer = servers.getServers().stream() + .filter(s -> id.equals(s.getId()) && s.getConfiguration() != null) + .findFirst(); + if (maybeServer.isPresent()) { + MavenSettings.ServerConfiguration serverConfig = maybeServer.get().getConfiguration(); + connectTimeout = serverConfig.getConnectTimeout(); + readTimeout = serverConfig.getReadTimeout(); + } else { + connectTimeout = null; + readTimeout = null; + } + } else { + connectTimeout = null; + readTimeout = null; + } } else { externalOnly = false; mirrorsOf = null; includedRepos = null; excludedRepos = null; + connectTimeout = null; + readTimeout = null; } } @@ -97,7 +124,7 @@ public static MavenRepository apply(Collection mirrors, M for (MavenRepositoryMirror mirror : mirrors) { MavenRepository mapped = mirror.apply(repo); if (mapped != repo) { - return mapped; + return mapped; } } return repo; @@ -110,7 +137,9 @@ public MavenRepository apply(MavenRepository repo) { .withReleases(!Boolean.FALSE.equals(releases) ? "true" : "false") .withSnapshots(!Boolean.FALSE.equals(snapshots) ? "true" : "false") // Since the URL has likely changed we cannot assume that the new repository is known to exist - .withKnownToExist(false); + .withKnownToExist(false) + .withConnectTimeout(repo.getConnectTimeout()) + .withReadTimeout(repo.getReadTimeout()); } return repo; } @@ -137,7 +166,7 @@ public boolean matches(MavenRepository repository) { } private boolean isInternal(MavenRepository repo) { - if (repo.getUri().regionMatches(true, 0,"file:", 0, 5)) { + if (repo.getUri().regionMatches(true, 0, "file:", 0, 5)) { return true; } try { diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/tree/ResolvedPom.java b/rewrite-maven/src/main/java/org/openrewrite/maven/tree/ResolvedPom.java index 97c66d4502a..a351c9bf890 100644 --- a/rewrite-maven/src/main/java/org/openrewrite/maven/tree/ResolvedPom.java +++ b/rewrite-maven/src/main/java/org/openrewrite/maven/tree/ResolvedPom.java @@ -733,6 +733,8 @@ private void mergeRepositories(List incomingRepositories) { incomingRepository.isKnownToExist(), incomingRepository.getUsername(), incomingRepository.getPassword(), + incomingRepository.getConnectTimeout(), + incomingRepository.getReadTimeout(), incomingRepository.getDeriveMetadataIfMissing() ); @@ -814,7 +816,7 @@ public List resolveDependencies(Scope scope, Map resolveDependencies(Scope scope, Map dependenciesAtNextDepth = new ArrayList<>(); for (DependencyAndDependent dd : dependenciesAtDepth) { - //First get the dependency (relative to the pom it was defined in) - Dependency d = dd.getDefinedIn().getValues(dd.getDependency(), depth); - //The dependency may be modified by the current pom's managed dependencies + // First get the dependency (relative to the pom it was defined in) + // Depth 0 prevents its dependency management from overriding versions of its own direct dependencies + Dependency d = dd.getDefinedIn().getValues(dd.getDependency(), 0); + // The dependency may be modified by the current pom's dependency management d = getValues(d, depth); try { if (d.getVersion() == null) { diff --git a/rewrite-maven/src/test/java/org/openrewrite/maven/AddRepositoryTest.java b/rewrite-maven/src/test/java/org/openrewrite/maven/AddRepositoryTest.java index 90b3da09f22..2dc106fa1a5 100644 --- a/rewrite-maven/src/test/java/org/openrewrite/maven/AddRepositoryTest.java +++ b/rewrite-maven/src/test/java/org/openrewrite/maven/AddRepositoryTest.java @@ -29,7 +29,7 @@ void addSimpleRepo() { rewriteRun( spec -> spec.recipe(new AddRepository("myRepo", "http://myrepo.maven.com/repo", null, null, null, null, null, - null, null, null)), + null, null, null, null)), pomXml( """ @@ -55,12 +55,43 @@ void addSimpleRepo() { ); } + @Test + void addSimplePluginRepo() { + rewriteRun( + spec -> spec.recipe(new AddRepository("myRepo", "http://myrepo.maven.com/repo", null, null, + null, null, null, + null, null, null, AddRepository.Type.PluginRepository)), + pomXml( + """ + + com.mycompany.app + my-app + 1 + + """, + """ + + com.mycompany.app + my-app + 1 + + + myRepo + http://myrepo.maven.com/repo + + + + """ + ) + ); + } + @Test void updateExistingRepo() { rewriteRun( spec -> spec.recipe(new AddRepository("myRepo", "http://myrepo.maven.com/repo", "bb", null, null, null, null, - null, null, null)), + null, null, null, null)), pomXml( """ @@ -99,7 +130,7 @@ void doNotRemoveRepoName() { rewriteRun( spec -> spec.recipe(new AddRepository("myRepo", "http://myrepo.maven.com/repo", null, null, null, null, null, - null, null, null)), + null, null, null, null)), pomXml( """ @@ -124,7 +155,7 @@ void removeSnapshots() { rewriteRun( spec -> spec.recipe(new AddRepository("myRepo", "http://myrepo.maven.com/repo", null, null, null, null, null, - null, null, null)), + null, null, null, null)), pomXml( """ @@ -164,7 +195,7 @@ void updateSnapshots1() { rewriteRun( spec -> spec.recipe(new AddRepository("myRepo", "http://myrepo.maven.com/repo", null, null, false, "whatever", null, - null, null, null)), + null, null, null, null)), pomXml( """ @@ -208,7 +239,7 @@ void updateSnapshots2() { rewriteRun( spec -> spec.recipe(new AddRepository("myRepo", "http://myrepo.maven.com/repo", null, null, null, "whatever", null, - null, null, null)), + null, null, null, null)), pomXml( """ @@ -251,7 +282,7 @@ void noIdMatch1SameSnapshots() { rewriteRun( spec -> spec.recipe(new AddRepository("myRepo", "http://myrepo.maven.com/repo", null, null, true, null, null, - null, null, null)), + null, null, null, null)), pomXml( """ @@ -280,7 +311,7 @@ void updateToSpringBoot30Snapshot() { spec -> spec.recipes( new AddRepository("boot-snapshots", "https://repo.spring.io/snapshot", null, null, true, null, null, - null, null, null), + null, null, null, null), new UpgradeParentVersion("org.springframework.boot", "spring-boot-starter-parent", "3.0.0-SNAPSHOT", null) ), pomXml( diff --git a/rewrite-maven/src/test/java/org/openrewrite/maven/MavenParserTest.java b/rewrite-maven/src/test/java/org/openrewrite/maven/MavenParserTest.java index 7fda40e4ddf..26763316dd8 100644 --- a/rewrite-maven/src/test/java/org/openrewrite/maven/MavenParserTest.java +++ b/rewrite-maven/src/test/java/org/openrewrite/maven/MavenParserTest.java @@ -2936,4 +2936,106 @@ void escapedA() { ) ); } + + @Test + void transitiveDependencyManagement() { + rewriteRun( + mavenProject("depends-on-guava", + pomXml(""" + + 4.0.0 + org.example + depends-on-guava + 0.0.1 + + + com.google.guava + guava + 29.0-jre + + + + + + com.google.guava + guava + 30.0-jre + + + + + """, + spec -> spec.afterRecipe(pom -> { + //noinspection OptionalGetWithoutIsPresent + List guava = pom.getMarkers().findFirst(MavenResolutionResult.class) + .map(mrr -> mrr.findDependencies("com.google.guava", "guava", Scope.Compile)) + .get(); + + assertThat(guava) + .singleElement() + .as("Dependency management cannot override the version of a direct dependency") + .matches(it -> "29.0-jre".equals(it.getVersion())); + }) + )), + mavenProject("transitively-depends-on-guava", + pomXml(""" + + 4.0.0 + org.example + transitively-depends-on-guava + 0.0.1 + + + org.example + depends-on-guava + 0.0.1 + + + + """, + spec -> spec.afterRecipe(pom -> { + //noinspection OptionalGetWithoutIsPresent + List guava = pom.getMarkers().findFirst(MavenResolutionResult.class) + .map(mrr -> mrr.findDependencies("com.google.guava", "guava", Scope.Compile)) + .get(); + + assertThat(guava) + .singleElement() + .as("The dependency management of dependency does not override the versions of its own direct dependencies") + .matches(it -> "29.0-jre".equals(it.getVersion())); + }) + ) + ) + ); + } + + @Test + void runtimeClasspathOnly() { + rewriteRun( + pomXml( + """ + + 4.0.0 + com.example + cache-2 + 0.0.1-SNAPSHOT + + + org.glassfish.jaxb + jaxb-runtime + 4.0.5 + runtime + + + + """, + spec -> spec.afterRecipe(pomXml -> { + MavenResolutionResult resolution = pomXml.getMarkers().findFirst(MavenResolutionResult.class).orElseThrow(); + assertThat(resolution.findDependencies("org.glassfish.jaxb", "jaxb-runtime", Scope.Runtime)).isNotEmpty(); + assertThat(resolution.findDependencies("org.glassfish.jaxb", "jaxb-runtime", Scope.Compile)).isEmpty(); + assertThat(resolution.findDependencies("jakarta.xml.bind", "jakarta.xml.bind-api", Scope.Compile)).isEmpty(); + }) + ) + ); + } } diff --git a/rewrite-maven/src/test/java/org/openrewrite/maven/MavenSettingsTest.java b/rewrite-maven/src/test/java/org/openrewrite/maven/MavenSettingsTest.java index 08c95599f42..0a5e8f6e249 100644 --- a/rewrite-maven/src/test/java/org/openrewrite/maven/MavenSettingsTest.java +++ b/rewrite-maven/src/test/java/org/openrewrite/maven/MavenSettingsTest.java @@ -15,18 +15,28 @@ */ package org.openrewrite.maven; +import com.fasterxml.jackson.annotation.JsonInclude; import org.assertj.core.api.Condition; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.assertj.core.api.ThrowingConsumer; import org.intellij.lang.annotations.Language; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.openrewrite.InMemoryExecutionContext; import org.openrewrite.Issue; import org.openrewrite.Parser; +import org.openrewrite.maven.internal.MavenXmlMapper; import org.openrewrite.maven.tree.MavenRepository; import org.openrewrite.maven.tree.MavenRepositoryMirror; +import org.openrewrite.xml.SemanticallyEqual; +import org.openrewrite.xml.XmlParser; +import org.openrewrite.xml.tree.Xml; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.lang.reflect.Field; import java.nio.file.Path; import java.nio.file.Paths; @@ -34,12 +44,16 @@ import static org.assertj.core.api.Assertions.assertThat; -@SuppressWarnings({"HttpUrlsUsage", "ConstantConditions"}) +@SuppressWarnings({"HttpUrlsUsage", "ConstantConditions", "OptionalGetWithoutIsPresent"}) class MavenSettingsTest { + private final MavenExecutionContextView ctx = MavenExecutionContextView.view( + new InMemoryExecutionContext((ThrowingConsumer) input -> { + throw input; + })); + @Test void parse() { - var ctx = MavenExecutionContextView.view(new InMemoryExecutionContext()); ctx.setMavenSettings(MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream( //language=xml """ @@ -73,7 +87,6 @@ void parse() { @Issue("https://github.com/openrewrite/rewrite/issues/131") @Test void defaultActiveWhenNoOthersAreActive() { - var ctx = MavenExecutionContextView.view(new InMemoryExecutionContext()); ctx.setMavenSettings(MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream( //language=xml """ @@ -114,7 +127,6 @@ void defaultActiveWhenNoOthersAreActive() { @Test void idCollisionLastRepositoryWins() { - var ctx = MavenExecutionContextView.view(new InMemoryExecutionContext()); ctx.setMavenSettings(MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream( //language=xml """ @@ -151,13 +163,12 @@ void idCollisionLastRepositoryWins() { assertThat(ctx.getRepositories()) .as("When multiple repositories have the same id in a maven settings file the last one wins. In a pom.xml an error would be thrown.") - .containsExactly(new MavenRepository("repo", "https://lastwins.com", null, null, null, null)); + .containsExactly(new MavenRepository("repo", "https://lastwins.com", null, null, null, null, null, null)); } @Issue("https://github.com/openrewrite/rewrite/issues/131") @Test void defaultOnlyActiveIfNoOthersAreActive() { - var ctx = MavenExecutionContextView.view(new InMemoryExecutionContext()); ctx.setMavenSettings(MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream( //language=xml """ @@ -198,7 +209,6 @@ void defaultOnlyActiveIfNoOthersAreActive() { """.getBytes() )), ctx)); - assertThat(ctx.getActiveProfiles()) .containsExactly("repo"); @@ -209,7 +219,6 @@ void defaultOnlyActiveIfNoOthersAreActive() { @Issue("https://github.com/openrewrite/rewrite/issues/130") @Test void mirrorReplacesRepository() { - var ctx = MavenExecutionContextView.view(new InMemoryExecutionContext()); ctx.setMavenSettings(MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream( //language=xml """ @@ -252,7 +261,6 @@ void mirrorReplacesRepository() { @Test void starredMirrorWithExclusion() { - var ctx = MavenExecutionContextView.view(new InMemoryExecutionContext()); ctx.setMavenSettings(MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream( //language=xml """ @@ -308,7 +316,6 @@ void starredMirrorWithExclusion() { @Test void serverCredentials() { - var ctx = MavenExecutionContextView.view(new InMemoryExecutionContext()); var settings = MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream( //language=xml """ @@ -334,13 +341,40 @@ void serverCredentials() { .matches(repo -> repo.getPassword().equals("my_password")); } + @Test + void serverTimeouts() { + var settings = MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream( + //language=xml + """ + + + + server001 + + 40000 + 50000 + + + + + """.getBytes() + )), ctx); + + assertThat(settings.getServers()).isNotNull(); + assertThat(settings.getServers().getServers()).hasSize(1); + assertThat(settings.getServers().getServers().get(0)) + .matches(repo -> repo.getId().equals("server001")) + .matches(repo -> repo.getConfiguration().getConnectTimeout().equals(40000L)) + .matches(repo -> repo.getConfiguration().getReadTimeout().equals(50000L)); + } + @Nested @Issue("https://github.com/openrewrite/rewrite/issues/1688") class LocalRepositoryTest { @Test - void parsesLocalRepositoryPathFromSettingsXml() { - var localRepoPath = System.getProperty("java.io.tmpdir"); - var ctx = MavenExecutionContextView.view(new InMemoryExecutionContext()); + void parsesLocalRepositoryPathFromSettingsXml(@TempDir Path localRepoPath) { ctx.setMavenSettings(MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream( //language=xml """ @@ -353,13 +387,11 @@ void parsesLocalRepositoryPathFromSettingsXml() { )), ctx)); assertThat(ctx.getLocalRepository().getUri()) .startsWith("file://") - .containsSubsequence(Paths.get(localRepoPath).toUri().toString().split("/")); + .containsSubsequence(localRepoPath.toUri().toString().split("/")); } @Test - void parsesLocalRepositoryUriFromSettingsXml() { - var localRepoPath = Paths.get(System.getProperty("java.io.tmpdir")).toUri().toString(); - var ctx = MavenExecutionContextView.view(new InMemoryExecutionContext()); + void parsesLocalRepositoryUriFromSettingsXml(@TempDir Path localRepoPath) { ctx.setMavenSettings(MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream( //language=xml """ @@ -373,12 +405,11 @@ void parsesLocalRepositoryUriFromSettingsXml() { assertThat(ctx.getLocalRepository().getUri()) .startsWith("file://") - .containsSubsequence(localRepoPath.split("/")); + .containsSubsequence(localRepoPath.toUri().toString().split("/")); } @Test void defaultsToTheMavenDefault() { - var ctx = MavenExecutionContextView.view(new InMemoryExecutionContext()); ctx.setMavenSettings(MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream( //language=xml """ @@ -432,7 +463,7 @@ void properties() { """.getBytes() - )), new InMemoryExecutionContext()); + )), ctx); assertThat(settings.getLocalRepository()).isEqualTo("/tmp/maven/local/repository/"); } @@ -472,7 +503,7 @@ void unresolvedPlaceholdersRemainUnchanged() { """.getBytes() - )), new InMemoryExecutionContext()); + )), ctx); assertThat(settings.getLocalRepository()) .isEqualTo("${custom.location.zz}/maven/local/repository/"); @@ -518,7 +549,7 @@ void env() { """.getBytes() - )), new InMemoryExecutionContext()); + )), ctx); assertThat(settings.getServers()).isNotNull(); assertThat(settings.getServers().getServers()).hasSize(1); @@ -591,7 +622,7 @@ class MergingTest { void concatenatesElementsWithUniqueIds() { Path path = Paths.get("settings.xml"); var baseSettings = MavenSettings.parse(new Parser.Input(path, () -> new ByteArrayInputStream( - installationSettings.getBytes())), new InMemoryExecutionContext()); + installationSettings.getBytes())), ctx); var userSettings = MavenSettings.parse(new Parser.Input(path, () -> new ByteArrayInputStream( //language=xml """ @@ -637,7 +668,7 @@ void concatenatesElementsWithUniqueIds() { """.getBytes() - )), new InMemoryExecutionContext()); + )), ctx); var mergedSettings = userSettings.merge(baseSettings); @@ -651,7 +682,7 @@ void concatenatesElementsWithUniqueIds() { void replacesElementsWithMatchingIds() { Path path = Paths.get("settings.xml"); var baseSettings = MavenSettings.parse(new Parser.Input(path, () -> new ByteArrayInputStream( - installationSettings.getBytes())), new InMemoryExecutionContext()); + installationSettings.getBytes())), ctx); var userSettings = MavenSettings.parse(new Parser.Input(path, () -> new ByteArrayInputStream( //language=xml """ @@ -686,7 +717,7 @@ void replacesElementsWithMatchingIds() { """.getBytes() - )), new InMemoryExecutionContext()); + )), ctx); var mergedSettings = userSettings.merge(baseSettings); @@ -742,9 +773,50 @@ void serverHttpHeaders() { """.getBytes() - )), new InMemoryExecutionContext()); + )), ctx); MavenSettings.Server server = settings.getServers().getServers().get(0); assertThat(server.getConfiguration().getHttpHeaders().get(0).getName()).isEqualTo("X-JFrog-Art-Api"); } + + @Test + void canDeserializeSettingsCorrectly() throws IOException { + Xml.Document parsed = (Xml.Document) XmlParser.builder().build().parse(""" + + + + maven-snapshots + + 10000 + + + X-JFrog-Art-Api + myApiToken + + + + + + + """).findFirst().get(); + + MavenSettings.HttpHeader httpHeader = new MavenSettings.HttpHeader("X-JFrog-Art-Api", "myApiToken"); + MavenSettings.ServerConfiguration configuration = new MavenSettings.ServerConfiguration(java.util.Collections.singletonList(httpHeader), 10000L, null); + MavenSettings.Server server = new MavenSettings.Server("maven-snapshots", null, null, configuration); + MavenSettings.Servers servers = new MavenSettings.Servers(java.util.Collections.singletonList(server)); + MavenSettings settings = new MavenSettings(null, null, null, null, servers); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + MavenXmlMapper.writeMapper() + .setSerializationInclusion(JsonInclude.Include.NON_ABSENT) + .writerWithDefaultPrettyPrinter() + .writeValue(baos, settings); + + assertThat(XmlParser.builder().build().parse(baos.toString()).findFirst()) + .isPresent() + .get(InstanceOfAssertFactories.type(Xml.Document.class)) + .isNotNull() + .satisfies(serialized -> assertThat(SemanticallyEqual.areEqual(parsed, serialized)).isTrue()) + .satisfies(serialized -> assertThat(serialized.printAll().replace("\r", "")).isEqualTo(parsed.printAll())); + } } diff --git a/rewrite-maven/src/test/java/org/openrewrite/maven/internal/MavenPomDownloaderTest.java b/rewrite-maven/src/test/java/org/openrewrite/maven/internal/MavenPomDownloaderTest.java index 13d0847e17d..45ee6b3998e 100755 --- a/rewrite-maven/src/test/java/org/openrewrite/maven/internal/MavenPomDownloaderTest.java +++ b/rewrite-maven/src/test/java/org/openrewrite/maven/internal/MavenPomDownloaderTest.java @@ -58,7 +58,7 @@ @SuppressWarnings({"HttpUrlsUsage"}) class MavenPomDownloaderTest { private final ExecutionContext ctx = HttpSenderExecutionContextView.view(new InMemoryExecutionContext()) - .setHttpSender(new HttpUrlConnectionSender(Duration.ofMillis(100), Duration.ofMillis(100))); + .setHttpSender(new HttpUrlConnectionSender(Duration.ofMillis(150), Duration.ofMillis(150))); private void mockServer(Integer responseCode, Consumer block) { try (MockWebServer mockRepo = new MockWebServer()) { @@ -92,7 +92,7 @@ void ossSonatype() { @Issue("https://github.com/openrewrite/rewrite/issues/3908") @Test void centralIdOverridesDefaultRepository() { - var ctx = MavenExecutionContextView.view(new InMemoryExecutionContext()); + var ctx = MavenExecutionContextView.view(this.ctx); ctx.setMavenSettings(MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream( //language=xml """ @@ -133,13 +133,13 @@ void centralIdOverridesDefaultRepository() { @Test void listenerRecordsRepository() { - var ctx = MavenExecutionContextView.view(new InMemoryExecutionContext()); + var ctx = MavenExecutionContextView.view(this.ctx); // Avoid actually trying to reach the made-up https://internalartifactrepository.yourorg.com for (MavenRepository repository : ctx.getRepositories()) { repository.setKnownToExist(true); } - MavenRepository nonexistentRepo = new MavenRepository("repo", "http://internalartifactrepository.yourorg.com", null, null, true, null, null, null); + MavenRepository nonexistentRepo = new MavenRepository("repo", "http://internalartifactrepository.yourorg.com", null, null, true, null, null, null, null, null); List attemptedUris = new ArrayList<>(); List discoveredRepositories = new ArrayList<>(); ctx.setResolutionListener(new ResolutionEventListener() { @@ -171,7 +171,7 @@ void listenerRecordsFailedRepositoryAccess() { var ctx = MavenExecutionContextView.view(new InMemoryExecutionContext()); // Avoid actually trying to reach a made-up URL String httpUrl = "http://%s.com".formatted(UUID.randomUUID()); - MavenRepository nonexistentRepo = new MavenRepository("repo", httpUrl, null, null, false, null, null, null); + MavenRepository nonexistentRepo = new MavenRepository("repo", httpUrl, null, null, false, null, null, null, null, null); Map attemptedUris = new HashMap<>(); List discoveredRepositories = new ArrayList<>(); ctx.setResolutionListener(new ResolutionEventListener() { @@ -194,7 +194,7 @@ public void repositoryAccessFailed(String uri, Throwable e) { @Test void mirrorsOverrideRepositoriesInPom() { - var ctx = MavenExecutionContextView.view(new InMemoryExecutionContext()); + var ctx = MavenExecutionContextView.view(this.ctx); ctx.setMavenSettings(MavenSettings.parse(new Parser.Input(Paths.get("settings.xml"), () -> new ByteArrayInputStream( //language=xml """ @@ -285,7 +285,7 @@ void retryConnectException() throws Throwable { try (MockWebServer server = new MockWebServer()) { server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.NO_RESPONSE)); server.enqueue(new MockResponse().setResponseCode(200).setBody("body")); - String body = new String(downloader.sendRequest(new HttpSender.Request(server.url("/test").url(), "request".getBytes(), HttpSender.Method.GET, Map.of()))); + String body = new String(downloader.sendRequest(new HttpSender.Request(server.url("/test").url(), "request".getBytes(), HttpSender.Method.GET, Map.of(), null, null))); assertThat(body).isEqualTo("body"); assertThat(server.getRequestCount()).isEqualTo(2); server.shutdown(); @@ -596,7 +596,7 @@ void deriveMetaDataFromFileRepository(@TempDir Path repoPath) throws IOException .knownToExist(true) .deriveMetadataIfMissing(true) .build(); - MavenMetadata metaData = new MavenPomDownloader(emptyMap(), new InMemoryExecutionContext()) + MavenMetadata metaData = new MavenPomDownloader(emptyMap(), ctx) .downloadMetadata(new GroupArtifact("fred", "fred"), null, List.of(repository)); assertThat(metaData.getVersioning().getVersions()).hasSize(3).containsAll(Arrays.asList("1.0.0", "1.1.0", "2.0.0")); } @@ -656,7 +656,7 @@ void mergeMetadata() throws IOException { var m1 = MavenMetadata.parse(metadata1.getBytes()); var m2 = MavenMetadata.parse(metadata2.getBytes()); - var merged = new MavenPomDownloader(emptyMap(), new InMemoryExecutionContext()).mergeMetadata(m1, m2); + var merged = new MavenPomDownloader(emptyMap(), ctx).mergeMetadata(m1, m2); assertThat(merged.getVersioning().getSnapshot().getTimestamp()).isEqualTo("20220927.033510"); assertThat(merged.getVersioning().getSnapshot().getBuildNumber()).isEqualTo("223"); @@ -694,7 +694,7 @@ void skipsLocalInvalidArtifactsMissingJar(@TempDir Path localRepository) throws // Does not return invalid dependency. assertThrows(MavenDownloadingException.class, () -> - new MavenPomDownloader(emptyMap(), new InMemoryExecutionContext()) + new MavenPomDownloader(emptyMap(), ctx) .download(new GroupArtifactVersion("com.bad", "bad-artifact", "1"), null, null, List.of(mavenLocal))); } @@ -727,7 +727,7 @@ void skipsLocalInvalidArtifactsEmptyJar(@TempDir Path localRepository) throws IO // Does not return invalid dependency. assertThrows(MavenDownloadingException.class, () -> - new MavenPomDownloader(emptyMap(), new InMemoryExecutionContext()) + new MavenPomDownloader(emptyMap(), ctx) .download(new GroupArtifactVersion("com.bad", "bad-artifact", "1"), null, null, List.of(mavenLocal))); } diff --git a/rewrite-maven/src/test/java/org/openrewrite/maven/tree/MavenRepositoryMirrorTest.java b/rewrite-maven/src/test/java/org/openrewrite/maven/tree/MavenRepositoryMirrorTest.java index 4e70fa1aac6..7b4bc8d9ba1 100644 --- a/rewrite-maven/src/test/java/org/openrewrite/maven/tree/MavenRepositoryMirrorTest.java +++ b/rewrite-maven/src/test/java/org/openrewrite/maven/tree/MavenRepositoryMirrorTest.java @@ -23,9 +23,9 @@ class MavenRepositoryMirrorTest { - MavenRepositoryMirror one = new MavenRepositoryMirror("one", "https://one.org/m2", "*", true, true); - MavenRepositoryMirror two = new MavenRepositoryMirror("two", "https://two.org/m2", "*", true, true); - MavenRepository foo = new MavenRepository("foo", "https://foo.org/m2", "true", "true", null, null); + MavenRepositoryMirror one = new MavenRepositoryMirror("one", "https://one.org/m2", "*", true, true, null); + MavenRepositoryMirror two = new MavenRepositoryMirror("two", "https://two.org/m2", "*", true, true, null); + MavenRepository foo = new MavenRepository("foo", "https://foo.org/m2", "true", "true", null, null, null, null); @Test void useFirstMirror() { @@ -37,7 +37,7 @@ void useFirstMirror() { @Test void matchById() { - MavenRepositoryMirror oneMirror = new MavenRepositoryMirror("mirror", "https://mirror", "one", true, true); + MavenRepositoryMirror oneMirror = new MavenRepositoryMirror("mirror", "https://mirror", "one", true, true, null); MavenRepository one = MavenRepository.builder() .id("one") @@ -59,7 +59,7 @@ void matchById() { @Test void excludeFromWildcard() { - MavenRepositoryMirror oneMirror = new MavenRepositoryMirror("mirror", "https://mirror", "*,!two", true, true); + MavenRepositoryMirror oneMirror = new MavenRepositoryMirror("mirror", "https://mirror", "*,!two", true, true, null); MavenRepository one = MavenRepository.builder() .id("one") .uri("https://one") diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/ChangeNamespaceValue.java b/rewrite-xml/src/main/java/org/openrewrite/xml/ChangeNamespaceValue.java index b821a95421e..6813ed343f2 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/ChangeNamespaceValue.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/ChangeNamespaceValue.java @@ -19,14 +19,26 @@ import lombok.Value; import org.openrewrite.*; import org.openrewrite.internal.ListUtils; +import org.openrewrite.internal.StringUtils; import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.marker.Markers; import org.openrewrite.xml.tree.Xml; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.openrewrite.Tree.randomId; + @Value @EqualsAndHashCode(callSuper = false) public class ChangeNamespaceValue extends Recipe { private static final String XMLNS_PREFIX = "xmlns"; private static final String VERSION_PREFIX = "version"; + private static final String SCHEMA_LOCATION_MATCH_PATTERN = "(?m)(.*)(%s)(\\s+)(.*)"; + private static final String SCHEMA_LOCATION_REPLACEMENT_PATTERN = "$1%s$3%s"; + private static final String MSG_TAG_UPDATED = "msg-tag-updated"; @Override public String getDisplayName() { @@ -65,22 +77,73 @@ public String getDescription() { String versionMatcher; @Nullable - @Option(displayName = "Search All Namespaces", + @Option(displayName = "Search all namespaces", description = "Specify whether evaluate all namespaces. Defaults to true", example = "true", required = false) Boolean searchAllNamespaces; + @Nullable + @Option(displayName = "New Resource version", + description = "The new version of the resource", + example = "2.0") + String newVersion; + + @Option(displayName = "Schema location", + description = "The new value to be used for the namespace schema location.", + example = "newfoo.bar.attribute.value.string", + required = false) + @Nullable + String newSchemaLocation; + + public static final String XML_SCHEMA_INSTANCE_PREFIX = "xsi"; + public static final String XML_SCHEMA_INSTANCE_URI = "http://www.w3.org/2001/XMLSchema-instance"; + + /** + * Find the tag that contains the declaration of the {@link #XML_SCHEMA_INSTANCE_URI} namespace. + * + * @param cursor the cursor to search from + * @return the tag that contains the declaration of the given namespace URI. + */ + public static Xml.Tag findTagContainingXmlSchemaInstanceNamespace(Cursor cursor) { + while (cursor != null) { + if (cursor.getValue() instanceof Xml.Document) { + return ((Xml.Document) cursor.getValue()).getRoot(); + } + Xml.Tag tag = cursor.firstEnclosing(Xml.Tag.class); + if (tag != null) { + if (tag.getNamespaces().containsValue(XML_SCHEMA_INSTANCE_URI)) { + return tag; + } + } + cursor = cursor.getParent(); + } + + // Should never happen + throw new IllegalArgumentException("Could not find tag containing namespace '" + XML_SCHEMA_INSTANCE_URI + "' or the enclosing Xml.Document instance."); + } + @Override public TreeVisitor getVisitor() { XPathMatcher elementNameMatcher = elementName != null ? new XPathMatcher(elementName) : null; return new XmlIsoVisitor() { + @Override + public Xml.Document visitDocument(Xml.Document document, ExecutionContext ctx) { + Xml.Document d = super.visitDocument(document, ctx); + if (ctx.pollMessage(MSG_TAG_UPDATED, false)) { + d = d.withRoot(addOrUpdateSchemaLocation(d.getRoot(), getCursor())); + } + return d; + } + @Override public Xml.Tag visitTag(Xml.Tag tag, ExecutionContext ctx) { Xml.Tag t = super.visitTag(tag, ctx); if (matchesElementName(getCursor()) && matchesVersion(t)) { t = t.withAttributes(ListUtils.map(t.getAttributes(), this::maybeReplaceNamespaceAttribute)); + t = t.withAttributes(ListUtils.map(t.getAttributes(), this::maybeReplaceVersionAttribute)); + ctx.putMessage(MSG_TAG_UPDATED, true); } return t; @@ -114,6 +177,18 @@ private Xml.Attribute maybeReplaceNamespaceAttribute(Xml.Attribute attribute) { return attribute; } + private Xml.Attribute maybeReplaceVersionAttribute(Xml.Attribute attribute) { + if (isVersionAttribute(attribute) && newVersion != null) { + return attribute.withValue( + new Xml.Attribute.Value(attribute.getId(), + "", + attribute.getMarkers(), + attribute.getValue().getQuote(), + newVersion)); + } + return attribute; + } + private boolean isXmlnsAttribute(Xml.Attribute attribute) { boolean searchAll = searchAllNamespaces == null || Boolean.TRUE.equals(searchAllNamespaces); return searchAll && attribute.getKeyAsString().startsWith(XMLNS_PREFIX) || @@ -129,6 +204,9 @@ private boolean isOldValue(Xml.Attribute attribute) { } private boolean isVersionMatch(Xml.Attribute attribute) { + if (versionMatcher == null) { + return true; + } String[] versions = versionMatcher.split(","); double dversion = Double.parseDouble(attribute.getValueAsString()); for (String splitVersion : versions) { @@ -149,6 +227,87 @@ private boolean isVersionMatch(Xml.Attribute attribute) { } return false; } + + private Xml.Tag addOrUpdateSchemaLocation(Xml.Tag root, Cursor cursor) { + if (StringUtils.isBlank(newSchemaLocation)) { + return root; + } + Xml.Tag newRoot = maybeAddNamespace(root); + Optional maybeSchemaLocation = maybeGetSchemaLocation(cursor, newRoot); + if (maybeSchemaLocation.isPresent() && oldValue != null) { + newRoot = updateSchemaLocation(newRoot, maybeSchemaLocation.get()); + } else if (!maybeSchemaLocation.isPresent()) { + newRoot = addSchemaLocation(newRoot); + } + return newRoot; + } + + private Optional maybeGetSchemaLocation(Cursor cursor, Xml.Tag tag) { + Xml.Tag schemaLocationTag = findTagContainingXmlSchemaInstanceNamespace(cursor); + Map namespaces = tag.getNamespaces(); + for (Xml.Attribute attribute : schemaLocationTag.getAttributes()) { + String attributeNamespace = namespaces.get(Xml.extractNamespacePrefix(attribute.getKeyAsString())); + if(XML_SCHEMA_INSTANCE_URI.equals(attributeNamespace) + && attribute.getKeyAsString().endsWith("schemaLocation")) { + return Optional.of(attribute); + } + } + + return Optional.empty(); + } + + private Xml.Tag maybeAddNamespace(Xml.Tag root) { + Map namespaces = root.getNamespaces(); + if (namespaces.containsValue(newValue) && !namespaces.containsValue(XML_SCHEMA_INSTANCE_URI)) { + namespaces.put(XML_SCHEMA_INSTANCE_PREFIX, XML_SCHEMA_INSTANCE_URI); + root = root.withNamespaces(namespaces); + } + return root; + } + + private Xml.Tag updateSchemaLocation(Xml.Tag newRoot, Xml.Attribute attribute) { + if(oldValue == null) { + return newRoot; + } + String oldSchemaLocation = attribute.getValueAsString(); + Matcher pattern = Pattern.compile(String.format(SCHEMA_LOCATION_MATCH_PATTERN, Pattern.quote(oldValue))) + .matcher(oldSchemaLocation); + if (pattern.find()) { + String newSchemaLocationValue = pattern.replaceFirst( + String.format(SCHEMA_LOCATION_REPLACEMENT_PATTERN, newValue, newSchemaLocation) + ); + Xml.Attribute newAttribute = attribute.withValue(attribute.getValue().withValue(newSchemaLocationValue)); + newRoot = newRoot.withAttributes(ListUtils.map(newRoot.getAttributes(), a -> a == attribute ? newAttribute : a)); + } + return newRoot; + } + + private Xml.Tag addSchemaLocation(Xml.Tag newRoot) { + return newRoot.withAttributes( + ListUtils.concat( + newRoot.getAttributes(), + new Xml.Attribute( + randomId(), + " ", + Markers.EMPTY, + new Xml.Ident( + randomId(), + "", + Markers.EMPTY, + String.format("%s:schemaLocation", XML_SCHEMA_INSTANCE_PREFIX) + ), + "", + new Xml.Attribute.Value( + randomId(), + "", + Markers.EMPTY, + Xml.Attribute.Value.Quote.Double, + String.format("%s %s", newValue, newSchemaLocation) + ) + ) + ) + ); + } }; } } diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/XPathMatcher.java b/rewrite-xml/src/main/java/org/openrewrite/xml/XPathMatcher.java index 9aa28ad3ec2..7c6066b72f9 100644 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/XPathMatcher.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/XPathMatcher.java @@ -37,7 +37,7 @@ public class XPathMatcher { // Regular expression to support conditional tags like `plugin[artifactId='maven-compiler-plugin']` or foo[@bar='baz'] - private static final Pattern PATTERN = Pattern.compile("([-\\w]+)\\[(@)?([-\\w]+)='([-\\w.]+)']"); + private static final Pattern PATTERN = Pattern.compile("([-\\w]+|\\*)\\[((local-name|namespace-uri)\\(\\)|(@)?([-\\w]+|\\*))='([-\\w.]+)']"); private final String expression; private final boolean startsWithSlash; @@ -82,6 +82,9 @@ public boolean matches(Cursor cursor) { if (part.charAt(index + 1) == '@') { partWithCondition = part; tagForCondition = path.get(i); + } else if (part.contains("(") && part.contains(")")) { //if is function + partWithCondition = part; + tagForCondition = path.get(i); } } else if (i < path.size() && i > 0 && parts[i - 1].endsWith("]")) { String partBefore = parts[i - 1]; @@ -94,6 +97,8 @@ public boolean matches(Cursor cursor) { partWithCondition = partBefore; tagForCondition = path.get(parts.length - i); } + } else if (part.endsWith(")")) { // is xpath method + // TODO: implement other xpath methods } String partName; @@ -101,7 +106,7 @@ public boolean matches(Cursor cursor) { Matcher matcher; if (tagForCondition != null && partWithCondition.endsWith("]") && (matcher = PATTERN.matcher( partWithCondition)).matches()) { - String optionalPartName = matchesCondition(matcher, tagForCondition); + String optionalPartName = matchesCondition(matcher, tagForCondition, cursor); if (optionalPartName == null) { return false; } @@ -176,7 +181,7 @@ public boolean matches(Cursor cursor) { Matcher matcher; if (tag != null && part.endsWith("]") && (matcher = PATTERN.matcher(part)).matches()) { - String optionalPartName = matchesCondition(matcher, tag); + String optionalPartName = matchesCondition(matcher, tag, cursor); if (optionalPartName == null) { return false; } @@ -191,7 +196,7 @@ public boolean matches(Cursor cursor) { "*".equals(part.substring(1))); } - if (path.size() < i + 1 || (tag != null && !tag.getName().equals(partName) && !"*".equals(part))) { + if (path.size() < i + 1 || (tag != null && !tag.getName().equals(partName) && !partName.equals("*") && !"*".equals(part))) { return false; } } @@ -201,21 +206,34 @@ public boolean matches(Cursor cursor) { } @Nullable - private String matchesCondition(Matcher matcher, Xml.Tag tag) { + private String matchesCondition(Matcher matcher, Xml.Tag tag, Cursor cursor) { String name = matcher.group(1); - boolean isAttribute = Objects.equals(matcher.group(2), "@"); - String selector = matcher.group(3); - String value = matcher.group(4); + boolean isAttribute = matcher.group(4) != null; // either group4 != null, or group 2 startsWith @ + String selector = isAttribute ? matcher.group(5) : matcher.group(2); + boolean isFunction = selector.endsWith("()"); + String value = matcher.group(6); boolean matchCondition = false; if (isAttribute) { for (Xml.Attribute a : tag.getAttributes()) { - if (a.getKeyAsString().equals(selector) && a.getValueAsString().equals(value)) { + if ((a.getKeyAsString().equals(selector) || "*".equals(selector)) && a.getValueAsString().equals(value)) { matchCondition = true; break; } } - } else { + } else if (isFunction) { + if (!name.equals("*") && !tag.getLocalName().equals(name)) { + matchCondition = false; + } else if (selector.equals("local-name()")) { + if (tag.getLocalName().equals(value)) { + matchCondition = true; + } + } else if (selector.equals("namespace-uri()")) { + if (tag.getNamespaceUri(cursor).get().equals(value)) { + matchCondition = true; + } + } + } else { // other [] conditions for (Xml.Tag t : FindTags.find(tag, selector)) { if (t.getValue().map(v -> v.equals(value)).orElse(false)) { matchCondition = true; diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/search/FindNamespacePrefix.java b/rewrite-xml/src/main/java/org/openrewrite/xml/search/FindNamespacePrefix.java new file mode 100644 index 00000000000..95623523636 --- /dev/null +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/search/FindNamespacePrefix.java @@ -0,0 +1,90 @@ +/* + * Copyright 2024 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.xml.search; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Option; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.internal.StringUtils; +import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.marker.SearchResult; +import org.openrewrite.xml.XPathMatcher; +import org.openrewrite.xml.XmlVisitor; +import org.openrewrite.xml.tree.Xml; + +import java.util.HashSet; +import java.util.Set; + +@Value +@EqualsAndHashCode(callSuper = false) +public class FindNamespacePrefix extends Recipe { + + @Option(displayName = "Namespace prefix", + description = "The Namespace Prefix to find.", + example = "http://www.w3.org/2001/XMLSchema-instance") + String namespacePrefix; + + @Option(displayName = "XPath", + description = "An XPath expression used to find namespace URIs.", + example = "/dependencies/dependency", + required = false) + @Nullable + String xPath; + + @Override + public String getDisplayName() { + return "Find XML namespace prefixes"; + } + + @Override + public String getDescription() { + return "Find XML namespace prefixes, optionally restricting the search by a XPath expression."; + } + + @Override + public TreeVisitor getVisitor() { + XPathMatcher matcher = StringUtils.isBlank(xPath) ? null : new XPathMatcher(xPath); + return new XmlVisitor() { + + @Override + public Xml visitTag(Xml.Tag tag, ExecutionContext ctx) { + Xml.Tag t = (Xml.Tag) super.visitTag(tag, ctx); + if (tag.getNamespaces().containsKey(namespacePrefix) && (matcher == null || matcher.matches(getCursor()))) { + t = SearchResult.found(t); + } + return t; + } + }; + } + + public static Set find(Xml x, String namespacePrefix, @Nullable String xPath) { + XPathMatcher matcher = StringUtils.isBlank(xPath) ? null : new XPathMatcher(xPath); + Set ts = new HashSet<>(); + new XmlVisitor>() { + @Override + public Xml visitTag(Xml.Tag tag, Set ts) { + if (tag.getNamespaces().containsKey(namespacePrefix) && (matcher == null || matcher.matches(getCursor()))) { + ts.add(tag); + } + return super.visitTag(tag, ts); + } + }.visit(x, ts); + return ts; + } +} diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/search/HasNamespaceUri.java b/rewrite-xml/src/main/java/org/openrewrite/xml/search/HasNamespaceUri.java new file mode 100644 index 00000000000..2745aff2264 --- /dev/null +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/search/HasNamespaceUri.java @@ -0,0 +1,90 @@ +/* + * Copyright 2024 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.xml.search; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Option; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.internal.StringUtils; +import org.openrewrite.internal.lang.Nullable; +import org.openrewrite.marker.SearchResult; +import org.openrewrite.xml.XPathMatcher; +import org.openrewrite.xml.XmlVisitor; +import org.openrewrite.xml.tree.Xml; + +import java.util.HashSet; +import java.util.Set; + +@Value +@EqualsAndHashCode(callSuper = false) +public class HasNamespaceUri extends Recipe { + + @Option(displayName = "Namespace URI", + description = "The Namespace URI to find.", + example = "http://www.w3.org/2001/XMLSchema-instance") + String namespaceUri; + + @Option(displayName = "XPath", + description = "An XPath expression used to find namespace URIs.", + example = "/dependencies/dependency", + required = false) + @Nullable + String xPath; + + @Override + public String getDisplayName() { + return "Find XML namespace URIs"; + } + + @Override + public String getDescription() { + return "Find XML namespace URIs, optionally restricting the search by a XPath expression."; + } + + @Override + public TreeVisitor getVisitor() { + XPathMatcher matcher = StringUtils.isBlank(xPath) ? null : new XPathMatcher(xPath); + return new XmlVisitor() { + + @Override + public Xml visitTag(Xml.Tag tag, ExecutionContext ctx) { + Xml.Tag t = (Xml.Tag) super.visitTag(tag, ctx); + if (tag.getNamespaces().containsValue(namespaceUri) && (matcher == null || matcher.matches(getCursor()))) { + t = SearchResult.found(t); + } + return t; + } + }; + } + + public static Set find(Xml x, String namespaceUri, @Nullable String xPath) { + XPathMatcher matcher = StringUtils.isBlank(xPath) ? null : new XPathMatcher(xPath); + Set ts = new HashSet<>(); + new XmlVisitor>() { + @Override + public Xml visitTag(Xml.Tag tag, Set ts) { + if (tag.getNamespaces().containsValue(namespaceUri) && (matcher == null || matcher.matches(getCursor()))) { + ts.add(tag); + } + return super.visitTag(tag, ts); + } + }.visit(x, ts); + return ts; + } +} diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/tree/Xml.java b/rewrite-xml/src/main/java/org/openrewrite/xml/tree/Xml.java index ff3be4ba794..caa74a3e842 100755 --- a/rewrite-xml/src/main/java/org/openrewrite/xml/tree/Xml.java +++ b/rewrite-xml/src/main/java/org/openrewrite/xml/tree/Xml.java @@ -20,6 +20,8 @@ import org.apache.commons.text.StringEscapeUtils; import org.intellij.lang.annotations.Language; import org.openrewrite.*; +import org.openrewrite.internal.ListUtils; +import org.openrewrite.internal.StringUtils; import org.openrewrite.internal.WhitespaceValidationService; import org.openrewrite.internal.lang.Nullable; import org.openrewrite.marker.Markers; @@ -32,10 +34,7 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Path; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.UUID; +import java.util.*; import java.util.stream.Collectors; import static java.util.Collections.emptyList; @@ -75,20 +74,61 @@ default

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

Xml acceptXml(XmlVisitor

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

Xml acceptXml(XmlVisitor

v, P p) { return v.visitTag(this, p); diff --git a/rewrite-xml/src/test/java/org/openrewrite/xml/ChangeNamespaceValueTest.java b/rewrite-xml/src/test/java/org/openrewrite/xml/ChangeNamespaceValueTest.java index fc668a0fb8a..38d6e10dfd4 100644 --- a/rewrite-xml/src/test/java/org/openrewrite/xml/ChangeNamespaceValueTest.java +++ b/rewrite-xml/src/test/java/org/openrewrite/xml/ChangeNamespaceValueTest.java @@ -27,7 +27,7 @@ class ChangeNamespaceValueTest implements RewriteTest { @Test void replaceVersion24Test() { rewriteRun( - spec -> spec.recipe(new ChangeNamespaceValue("web-app", null, "http://java.sun.com/xml/ns/j2ee", "2.4", false)) + spec -> spec.recipe(new ChangeNamespaceValue("web-app", null, "http://java.sun.com/xml/ns/j2ee", "2.4", false, null, null)) .expectedCyclesThatMakeChanges(2), xml( """ @@ -53,7 +53,7 @@ void replaceVersion24Test() { @Test void replaceVersion25Test() { rewriteRun( - spec -> spec.recipe(new ChangeNamespaceValue("web-app", null, "http://java.sun.com/xml/ns/java", "2.5,3.0", false)) + spec -> spec.recipe(new ChangeNamespaceValue("web-app", null, "http://java.sun.com/xml/ns/java", "2.5,3.0", false, null, null)) .expectedCyclesThatMakeChanges(2), xml( """ @@ -79,7 +79,7 @@ void replaceVersion25Test() { @Test void replaceVersion30Test() { rewriteRun( - spec -> spec.recipe(new ChangeNamespaceValue("web-app", null, "http://java.sun.com/xml/ns/java", "2.5,3.0", false)) + spec -> spec.recipe(new ChangeNamespaceValue("web-app", null, "http://java.sun.com/xml/ns/java", "2.5,3.0", false, null, null)) .expectedCyclesThatMakeChanges(2), xml( """ @@ -105,7 +105,7 @@ void replaceVersion30Test() { @Test void replaceVersion31Test() { rewriteRun( - spec -> spec.recipe(new ChangeNamespaceValue("web-app", null, "http://xmlns.jcp.org/xml/ns/javaee", "3.1+", false)) + spec -> spec.recipe(new ChangeNamespaceValue("web-app", null, "http://xmlns.jcp.org/xml/ns/javaee", "3.1+", false, null, null)) .expectedCyclesThatMakeChanges(2), xml( """ @@ -131,7 +131,7 @@ void replaceVersion31Test() { @Test void replaceVersion32Test() { rewriteRun( - spec -> spec.recipe(new ChangeNamespaceValue("web-app", null, "http://xmlns.jcp.org/xml/ns/javaee", "3.1+", false)) + spec -> spec.recipe(new ChangeNamespaceValue("web-app", null, "http://xmlns.jcp.org/xml/ns/javaee", "3.1+", false, null, null)) .expectedCyclesThatMakeChanges(2), xml( """ @@ -157,7 +157,7 @@ void replaceVersion32Test() { @Test void invalidVersionTest() { rewriteRun( - spec -> spec.recipe(new ChangeNamespaceValue("web-app", null, "http://java.sun.com/xml/ns/j2ee", "2.5", false)), + spec -> spec.recipe(new ChangeNamespaceValue("web-app", null, "http://java.sun.com/xml/ns/j2ee", "2.5", false, null, null)), xml( """ spec.recipe(new ChangeNamespaceValue(null, "http://old.namespace", "https://new.namespace", null, true)), + spec -> spec.recipe(new ChangeNamespaceValue(null, "http://old.namespace", "https://new.namespace", null, true, null, null)), xml( """ spec.recipe(new ChangeNamespaceValue(null, "http://old.namespace", "https://new.namespace", null, true)), + spec -> spec.recipe(new ChangeNamespaceValue(null, "http://old.namespace", "https://new.namespace", null, true, null, null)), xml( """ spec.recipe(new ChangeNamespaceValue(null, "http://non.existant.namespace", "https://new.namespace", null, true)), + spec -> spec.recipe(new ChangeNamespaceValue(null, "http://non.existant.namespace", "https://new.namespace", null, true, null, null)), xml( """ spec.recipe(new ChangeNamespaceValue("web-app", "http://java.sun.com/xml/ns/j2ee", "http://java.sun.com/xml/ns/javaee", "2.4", true, "2.5", "http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd")), + xml( + """ + + testWebDDNamespace + + """, + """ + + testWebDDNamespace + + """ + ) + ); + } + + @Test + void replaceNamespaceUriAndAddMissingSchemaLocation() { + rewriteRun( + spec -> spec.recipe(new ChangeNamespaceValue("web-app", "http://java.sun.com/xml/ns/j2ee", "http://java.sun.com/xml/ns/javaee", "2.4", true, "2.5", "http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd")), + xml( + """ + + testWebDDNamespace + + """, + """ + + testWebDDNamespace + + """ + ) + ); + } } diff --git a/rewrite-xml/src/test/java/org/openrewrite/xml/CreateXmlFileTest.java b/rewrite-xml/src/test/java/org/openrewrite/xml/CreateXmlFileTest.java index 843b3a66b6f..02e216acd83 100644 --- a/rewrite-xml/src/test/java/org/openrewrite/xml/CreateXmlFileTest.java +++ b/rewrite-xml/src/test/java/org/openrewrite/xml/CreateXmlFileTest.java @@ -15,6 +15,7 @@ */ package org.openrewrite.xml; +import org.intellij.lang.annotations.Language; import org.junit.jupiter.api.Test; import org.openrewrite.DocumentExample; import org.openrewrite.test.RewriteTest; @@ -26,6 +27,7 @@ class CreateXmlFileTest implements RewriteTest { @DocumentExample @Test void hasCreatedFile() { + @Language("xml") String fileContents = """ diff --git a/rewrite-xml/src/test/java/org/openrewrite/xml/XPathMatcherTest.java b/rewrite-xml/src/test/java/org/openrewrite/xml/XPathMatcherTest.java index 0836406afad..1fefbf336ae 100755 --- a/rewrite-xml/src/test/java/org/openrewrite/xml/XPathMatcherTest.java +++ b/rewrite-xml/src/test/java/org/openrewrite/xml/XPathMatcherTest.java @@ -159,20 +159,44 @@ void matchPom() { pomXml2)).isTrue(); } + private final SourceFile attributeXml = new XmlParser().parse( + """ + + + baz + + """ + ).toList().get(0); + @Test void attributePredicate() { - SourceFile xml = new XmlParser().parse( - """ - - - baz - - """ - ).toList().get(0); - assertThat(match("/root/element1[@foo='bar']", xml)).isTrue(); - assertThat(match("/root/element1[@foo='baz']", xml)).isFalse(); - assertThat(match("/root/element1[foo='bar']", xml)).isFalse(); - assertThat(match("/root/element1[foo='baz']", xml)).isTrue(); + assertThat(match("/root/element1[@foo='bar']", attributeXml)).isTrue(); + assertThat(match("/root/element1[@foo='baz']", attributeXml)).isFalse(); + assertThat(match("/root/element1[foo='bar']", attributeXml)).isFalse(); + assertThat(match("/root/element1[foo='baz']", attributeXml)).isTrue(); + } + + @Test + void wildcards() { + // condition with wildcard attribute + assertThat(match("/root/element1[@*='bar']", attributeXml)).isTrue(); + assertThat(match("/root/element1[@*='baz']", attributeXml)).isFalse(); + + // condition with wildcard element + assertThat(match("/root/element1[*='bar']", attributeXml)).isFalse(); + assertThat(match("/root/element1[*='baz']", attributeXml)).isTrue(); + + // absolute xpath with wildcard element + assertThat(match("/root/*[@foo='bar']", attributeXml)).isTrue(); + assertThat(match("/root/*[@*='bar']", attributeXml)).isTrue(); + assertThat(match("/root/*[@foo='baz']", attributeXml)).isFalse(); + assertThat(match("/root/*[@*='baz']", attributeXml)).isFalse(); + + // relative xpath with wildcard element + assertThat(match("//*[@foo='bar']", attributeXml)).isTrue(); + assertThat(match("//*[@foo='baz']", attributeXml)).isFalse(); +// assertThat(match("//*[foo='bar']", attributeXml)).isFalse(); // TODO: fix relative xpath with condition + assertThat(match("//*[foo='baz']", attributeXml)).isTrue(); } @Test @@ -188,6 +212,8 @@ void relativePathsWithConditions() { """ ).toList().get(0); +// assertThat(match("//element1[foo='bar']", xml)).isFalse(); // TODO: fix - was already failing before * changes + assertThat(match("//element1[foo='baz']", xml)).isTrue(); assertThat(match("//element1[@foo='bar']", xml)).isTrue(); assertThat(match("//element1[foo='baz']/test", xml)).isTrue(); assertThat(match("//element1[foo='baz']/baz", xml)).isFalse(); @@ -204,7 +230,7 @@ void matchFunctions() { // Namespace functions assertThat(match("/*[local-name()='element1']", namespacedXml)).isFalse(); - assertThat(match("//*[local-name()='element1']", namespacedXml)).isFalse(); + assertThat(match("//*[local-name()='element1']", namespacedXml)).isTrue(); assertThat(match("/root/*[local-name()='element1']", namespacedXml)).isTrue(); assertThat(match("/root/*[namespace-uri()='http://www.example.com/namespace2']", namespacedXml)).isTrue(); assertThat(match("/*[namespace-uri()='http://www.example.com/namespace2']", namespacedXml)).isFalse(); @@ -223,6 +249,22 @@ void matchFunctions() { assertThat(match("count(/root/*)", namespacedXml)).isTrue(); } + @Test + void testMatchLocalName() { + assertThat(match("/*[local-name()='root']", namespacedXml)).isTrue(); + assertThat(match("/*[local-name()='element1']", namespacedXml)).isFalse(); + assertThat(match("/*[local-name()='element2']", namespacedXml)).isFalse(); + assertThat(match("//*[local-name()='element1']", namespacedXml)).isTrue(); + assertThat(match("//*[local-name()='element2']", namespacedXml)).isTrue(); + assertThat(match("//*[local-name()='dne']", namespacedXml)).isFalse(); + + assertThat(match("/root[local-name()='root']", namespacedXml)).isTrue(); + assertThat(match("//element1[local-name()='element1']", namespacedXml)).isTrue(); + assertThat(match("//element2[local-name()='element2']", namespacedXml)).isFalse(); + assertThat(match("//ns2:element2[local-name()='element2']", namespacedXml)).isTrue(); + assertThat(match("//dne[local-name()='dne']", namespacedXml)).isFalse(); + } + private boolean match(String xpath, SourceFile x) { XPathMatcher matcher = new XPathMatcher(xpath); return !TreeVisitor.collect(new XmlVisitor<>() { diff --git a/rewrite-xml/src/test/java/org/openrewrite/xml/internal/XmlTest.java b/rewrite-xml/src/test/java/org/openrewrite/xml/internal/XmlTest.java new file mode 100644 index 00000000000..d029151e856 --- /dev/null +++ b/rewrite-xml/src/test/java/org/openrewrite/xml/internal/XmlTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2024 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.xml.internal; + +import org.junit.jupiter.api.Test; +import org.openrewrite.xml.tree.Xml; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class XmlTest { + + @Test + void isNamespaceDefinitionAttributeTests() { + assertThat(Xml.isNamespaceDefinitionAttribute("xmlns:test")).isTrue(); + assertThat(Xml.isNamespaceDefinitionAttribute("test")).isFalse(); + } + + @Test + void getAttributeNameForPrefix() { + assertThat(Xml.getAttributeNameForPrefix("test")).isEqualTo("xmlns:test"); + assertThat(Xml.getAttributeNameForPrefix("")).isEqualTo("xmlns"); + } + + @Test + void extractNamespacePrefix() { + assertEquals("test", Xml.extractNamespacePrefix("test:tag")); + assertEquals("", Xml.extractNamespacePrefix("tag")); + } + + @Test + void extractLocalName() { + assertEquals("tag", Xml.extractLocalName("test:tag")); + assertEquals("tag", Xml.extractLocalName("tag")); + } + + @Test + void extractPrefixFromNamespaceDefinition() { + assertEquals("test", Xml.extractPrefixFromNamespaceDefinition("xmlns:test")); + assertEquals("", Xml.extractPrefixFromNamespaceDefinition("xmlns")); + assertThat(Xml.extractPrefixFromNamespaceDefinition("test")).isEqualTo(null); + assertThat(Xml.extractPrefixFromNamespaceDefinition("a:test")).isEqualTo(null); + } +} diff --git a/rewrite-xml/src/test/java/org/openrewrite/xml/search/FindNamespacePrefixTest.java b/rewrite-xml/src/test/java/org/openrewrite/xml/search/FindNamespacePrefixTest.java new file mode 100644 index 00000000000..2e76cdb0c49 --- /dev/null +++ b/rewrite-xml/src/test/java/org/openrewrite/xml/search/FindNamespacePrefixTest.java @@ -0,0 +1,123 @@ +/* + * Copyright 2020 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.xml.search; + +import org.intellij.lang.annotations.Language; +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.test.RewriteTest; +import org.openrewrite.xml.tree.Xml; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.openrewrite.xml.Assertions.xml; + +class FindNamespacePrefixTest implements RewriteTest { + + @DocumentExample + @Test + void rootElement() { + rewriteRun( + spec -> spec.recipe(new FindNamespacePrefix("xsi", null)), + xml( + source, + """ + + + + + + + + + """ + ) + ); + } + + @Test + void nestedElement() { + rewriteRun( + spec -> spec.recipe(new FindNamespacePrefix("jaxws", null)), + xml( + source, + """ + + + + + + + + + """ + ) + ); + } + + @Test + void noMatchOnNamespacePrefix() { + rewriteRun( + spec -> spec.recipe(new FindNamespacePrefix("foo", null)), + xml(source) + ); + } + + @Test + void noMatchOnXPath() { + rewriteRun( + spec -> spec.recipe(new FindNamespacePrefix("xsi", "/jaxws:client")), + xml(source) + ); + } + + @Test + void staticFind() { + rewriteRun( + xml( + source, + spec -> spec.beforeRecipe(xml -> assertThat(FindNamespacePrefix.find(xml, "xsi", null)) + .isNotEmpty() + .hasSize(1) + .hasOnlyElementsOfType(Xml.Tag.class) + ) + ) + ); + } + + @Language("xml") + private final String source = """ + + + + + + + + + """; +} diff --git a/rewrite-xml/src/test/java/org/openrewrite/xml/search/HasNamespaceUriTest.java b/rewrite-xml/src/test/java/org/openrewrite/xml/search/HasNamespaceUriTest.java new file mode 100644 index 00000000000..fe788ae0465 --- /dev/null +++ b/rewrite-xml/src/test/java/org/openrewrite/xml/search/HasNamespaceUriTest.java @@ -0,0 +1,123 @@ +/* + * Copyright 2020 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.xml.search; + +import org.intellij.lang.annotations.Language; +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.test.RewriteTest; +import org.openrewrite.xml.tree.Xml; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.openrewrite.xml.Assertions.xml; + +class HasNamespaceUriTest implements RewriteTest { + + @DocumentExample + @Test + void rootElement() { + rewriteRun( + spec -> spec.recipe(new HasNamespaceUri("http://www.w3.org/2001/XMLSchema-instance", null)), + xml( + source, + """ + + + + + + + + + """ + ) + ); + } + + @Test + void nestedElement() { + rewriteRun( + spec -> spec.recipe(new HasNamespaceUri("http://cxf.apache.org/jaxws", null)), + xml( + source, + """ + + + + + + + + + """ + ) + ); + } + + @Test + void noMatchOnNamespaceUri() { + rewriteRun( + spec -> spec.recipe(new HasNamespaceUri("foo", null)), + xml(source) + ); + } + + @Test + void noMatchOnXPath() { + rewriteRun( + spec -> spec.recipe(new HasNamespaceUri("xsi", "/jaxws:client")), + xml(source) + ); + } + + @Test + void staticFind() { + rewriteRun( + xml( + source, + spec -> spec.beforeRecipe(xml -> assertThat(HasNamespaceUri.find(xml, "http://www.w3.org/2001/XMLSchema-instance", null)) + .isNotEmpty() + .hasSize(1) + .hasOnlyElementsOfType(Xml.Tag.class) + ) + ) + ); + } + + @Language("xml") + private final String source = """ + + + + + + + + + """; +} diff --git a/rewrite.yml b/rewrite.yml deleted file mode 100644 index 8d4317863f3..00000000000 --- a/rewrite.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -type: specs.openrewrite.org/v1beta/recipe -name: org.openrewrite.self.Rewrite -displayName: The rewrite recipes that rewrite rewrite recipes -description: > - The set of recipes that run against the rewrite codebase itself. -recipeList: - - org.openrewrite.text.EndOfLineAtEndOfFile diff --git a/settings.gradle.kts b/settings.gradle.kts index da0097cabbe..9712f7662de 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -103,6 +103,11 @@ develocity { capture { fileFingerprints = true } + publishing { + onlyIf { + authenticated + } + } uploadInBackground = !isCiServer }