diff --git a/rewrite-maven/src/main/java/org/openrewrite/maven/AddRuntimeConfig.java b/rewrite-maven/src/main/java/org/openrewrite/maven/AddRuntimeConfig.java
new file mode 100644
index 00000000000..132506987d3
--- /dev/null
+++ b/rewrite-maven/src/main/java/org/openrewrite/maven/AddRuntimeConfig.java
@@ -0,0 +1,172 @@
+/*
+ * 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.maven;
+
+import lombok.*;
+import org.openrewrite.*;
+import org.openrewrite.internal.StringUtils;
+import org.openrewrite.internal.lang.Nullable;
+import org.openrewrite.text.PlainText;
+import org.openrewrite.text.PlainTextVisitor;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+@Value
+@EqualsAndHashCode(callSuper = false)
+public class AddRuntimeConfig extends ScanningRecipe {
+ static final String POM_FILENAME = "pom.xml";
+ static final String MVN_CONFIG_DIR = ".mvn";
+ static final String MAVEN_CONFIG_FILENAME = "maven.config";
+ static final String MAVEN_CONFIG_PATH = MVN_CONFIG_DIR + "/" + MAVEN_CONFIG_FILENAME;
+ static final String JVM_CONFIG_FILENAME = "jvm.config";
+ static final String JVM_CONFIG_PATH = MVN_CONFIG_DIR + "/" + JVM_CONFIG_FILENAME;
+
+ @Option(displayName = "Config file",
+ description = "The file name for setting the runtime configuration.",
+ valid = {MAVEN_CONFIG_FILENAME, JVM_CONFIG_FILENAME},
+ example = "maven.config")
+ String relativeConfigFileName;
+
+ @Option(displayName = "Runtime flag",
+ description = "The runtime flag name to be set.",
+ example = "-T")
+ String flag;
+
+ @Option(displayName = "Runtime flag argument",
+ description = "The argument to set for the runtime flag. Some flags do not need to provide a value.",
+ required = false,
+ example = "3")
+ @Nullable
+ String argument;
+
+ @Option(displayName = "Separator between runtime flag and argument",
+ description = "The separator to use if flag and argument have been provided.",
+ valid = {"", " ", "="},
+ example = "=")
+ Separator separator;
+
+ @Getter
+ public enum Separator {
+ NONE(""),
+ SPACE(" "),
+ EQUALS("=");
+
+ private final String notation;
+
+ Separator(String notation) {
+ this.notation = notation;
+ }
+ }
+
+ @Override
+ public String getDisplayName() {
+ return "Add a configuration option for the Maven runtime";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Add a new configuration option for the Maven runtime if not already present.";
+ }
+
+ @Data
+ @RequiredArgsConstructor
+ public static class Accumulator {
+ final String targetRepresentation;
+ boolean mavenProject;
+ Path matchingRuntimeConfigFile;
+
+ }
+
+ @Override
+ public Accumulator getInitialValue(ExecutionContext ctx) {
+ String targetRepresentation = argument == null ? flag : flag + separator.getNotation() + argument;
+ return new Accumulator(targetRepresentation);
+ }
+
+ @Override
+ public TreeVisitor, ExecutionContext> getScanner(Accumulator acc) {
+ return new TreeVisitor() {
+ @Override
+ public Tree preVisit(Tree tree, ExecutionContext ctx) {
+ stopAfterPreVisit();
+ if (tree instanceof SourceFile) {
+ Path sourcePath = ((SourceFile) tree).getSourcePath();
+ switch (PathUtils.separatorsToUnix(sourcePath.toString())) {
+ case POM_FILENAME:
+ acc.setMavenProject(true);
+ break;
+ case MAVEN_CONFIG_PATH:
+ case JVM_CONFIG_PATH:
+ acc.setMatchingRuntimeConfigFile(sourcePath);
+ break;
+ default:
+ break;
+ }
+ }
+ return tree;
+ }
+ };
+ }
+
+ @Override
+ public Collection extends SourceFile> generate(Accumulator acc, ExecutionContext ctx) {
+ if (acc.isMavenProject() && acc.getMatchingRuntimeConfigFile() == null) {
+ return Collections.singletonList(PlainText.builder()
+ .text(acc.getTargetRepresentation())
+ .sourcePath(Paths.get(MVN_CONFIG_DIR, relativeConfigFileName))
+ .build());
+ }
+ return Collections.emptyList();
+ }
+
+ @Override
+ public TreeVisitor, ExecutionContext> getVisitor(Accumulator acc) {
+ return Preconditions.check(acc.isMavenProject() && acc.getMatchingRuntimeConfigFile() != null,
+ new PlainTextVisitor() {
+ @Override
+ public PlainText visitText(PlainText plainText, ExecutionContext ctx) {
+ if (plainText.getSourcePath().equals(acc.getMatchingRuntimeConfigFile())) {
+ return addOrReplaceConfig(plainText, acc);
+ }
+ return plainText;
+ }
+
+ private PlainText addOrReplaceConfig(PlainText plainText, Accumulator acc) {
+ String existingContent = plainText.getText();
+ Matcher matcher = Pattern.compile(Pattern.quote(flag) + "[=\\s]?[a-zA-Z0-9]*").matcher(existingContent);
+ if (matcher.find()) {
+ return plainText.withText(matcher.replaceAll(acc.getTargetRepresentation()));
+ }
+
+ String newText = StringUtils.isBlank(existingContent) ? existingContent : existingContent + determineConfigSeparator(plainText);
+ return plainText.withText(newText + acc.getTargetRepresentation());
+ }
+
+ private String determineConfigSeparator(PlainText plainText) {
+ // Use new line for maven.config, space for jvm.config
+ if (Paths.get(JVM_CONFIG_PATH).equals(plainText.getSourcePath())) {
+ return " ";
+ }
+ return plainText.getText().contains("\r\n") ? "\r\n" : "\n";
+ }
+ });
+ }
+}
diff --git a/rewrite-maven/src/test/java/org/openrewrite/maven/AddRuntimeConfigTest.java b/rewrite-maven/src/test/java/org/openrewrite/maven/AddRuntimeConfigTest.java
new file mode 100644
index 00000000000..48774a1a182
--- /dev/null
+++ b/rewrite-maven/src/test/java/org/openrewrite/maven/AddRuntimeConfigTest.java
@@ -0,0 +1,175 @@
+/*
+ * 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.maven;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.openrewrite.DocumentExample;
+import org.openrewrite.test.RewriteTest;
+import org.openrewrite.test.SourceSpecs;
+
+import static org.openrewrite.maven.AddRuntimeConfig.*;
+import static org.openrewrite.maven.Assertions.pomXml;
+import static org.openrewrite.test.SourceSpecs.text;
+
+class AddRuntimeConfigTest implements RewriteTest {
+ private static final SourceSpecs POM_XML_SOURCE_SPEC = pomXml(
+ """
+
+ com.mycompany.app
+ my-app
+ 1
+
+ """
+ );
+
+ @Test
+ @DocumentExample
+ void createConfigFileWithRuntimeConfigIfFileDoesNotExist() {
+ rewriteRun(
+ spec -> spec.recipe(new AddRuntimeConfig(MAVEN_CONFIG_FILENAME, "-T", "3", Separator.EQUALS)),
+ POM_XML_SOURCE_SPEC,
+ text(
+ null,
+ "-T=3",
+ spec -> spec.path(MAVEN_CONFIG_PATH)
+ )
+ );
+ }
+
+ @Test
+ void appendRuntimeFlagToEmptyConfigFile() {
+ rewriteRun(
+ spec -> spec.recipe(new AddRuntimeConfig(MAVEN_CONFIG_FILENAME, "-T", "3", Separator.EQUALS)),
+ POM_XML_SOURCE_SPEC,
+ text(
+ "",
+ "-T=3",
+ spec -> spec.path(MAVEN_CONFIG_PATH)
+ )
+ );
+ }
+
+ @ParameterizedTest
+ @EnumSource(Separator.class)
+ void createConfigFileWithRuntimeConfigForAllSeparators(Separator separator) {
+ rewriteRun(
+ spec -> spec.recipe(new AddRuntimeConfig(MAVEN_CONFIG_FILENAME, "-T", "3", separator)),
+ POM_XML_SOURCE_SPEC,
+ text(
+ "",
+ "-T" + separator.getNotation() + "3",
+ spec -> spec.path(MAVEN_CONFIG_PATH)
+ )
+ );
+ }
+
+ @Test
+ void appendRuntimeFlagIfItDoesNotExist() {
+ rewriteRun(
+ spec -> spec.recipe(new AddRuntimeConfig(MAVEN_CONFIG_FILENAME, "-T", "3", Separator.EQUALS)),
+ POM_XML_SOURCE_SPEC,
+ text(
+ "-U",
+ """
+ -U
+ -T=3
+ """,
+ spec -> spec.path(MAVEN_CONFIG_PATH)
+ )
+ );
+ }
+
+ @Test
+ void doesNotModifyRuntimeFlagIfExistingWithoutArgument() {
+ rewriteRun(
+ spec -> spec.recipe(new AddRuntimeConfig(MAVEN_CONFIG_FILENAME, "-U", null, Separator.EQUALS)),
+ POM_XML_SOURCE_SPEC,
+ text(
+ "-U",
+ spec -> spec.path(MAVEN_CONFIG_PATH)
+ )
+ );
+ }
+
+ @Test
+ void doesNotModifyRuntimeFlagIfExistingWithSameArgument() {
+ rewriteRun(
+ spec -> spec.recipe(new AddRuntimeConfig(MAVEN_CONFIG_FILENAME, "-T", "3", Separator.EQUALS)),
+ POM_XML_SOURCE_SPEC,
+ text(
+ "-T=3",
+ spec -> spec.path(MAVEN_CONFIG_PATH)
+ )
+ );
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"--threads=2", "--threads=3"})
+ void appendRuntimeFlagIfExistingForFlagFormatMismatch(String existingConfig) {
+ rewriteRun(
+ spec -> spec.recipe(new AddRuntimeConfig(MAVEN_CONFIG_FILENAME, "-T", "3", Separator.EQUALS)),
+ POM_XML_SOURCE_SPEC,
+ text(
+ existingConfig,
+ existingConfig + System.lineSeparator() + "-T=3",
+ spec -> spec.path(MAVEN_CONFIG_PATH)
+ )
+ );
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"-T 3", "-T3", "-T=3"})
+ void replaceRuntimeFlagIfExistingWithDifferentArgument(String existingConfig) {
+ rewriteRun(
+ spec -> spec.recipe(new AddRuntimeConfig(MAVEN_CONFIG_FILENAME, "-T", "4", Separator.EQUALS)),
+ POM_XML_SOURCE_SPEC,
+ text(
+ existingConfig,
+ "-T=4",
+ spec -> spec.path(MAVEN_CONFIG_PATH)
+ )
+ );
+ }
+
+ @Test
+ void addJvmRuntimeFlagOnTheSameLine() {
+ rewriteRun(
+ spec -> spec.recipe(new AddRuntimeConfig(JVM_CONFIG_FILENAME, "-XX:MaxPermSize", "512m", Separator.EQUALS)),
+ POM_XML_SOURCE_SPEC,
+ text(
+ "-Xmx2048m -Xms1024m",
+ "-Xmx2048m -Xms1024m -XX:MaxPermSize=512m",
+ spec -> spec.path(JVM_CONFIG_PATH)
+ )
+ );
+ }
+
+ @Test
+ void replaceJvmRuntimeFlagOnTheSameLine() {
+ rewriteRun(
+ spec -> spec.recipe(new AddRuntimeConfig(JVM_CONFIG_FILENAME, "-XX:MaxPermSize", "1024m", Separator.EQUALS)),
+ POM_XML_SOURCE_SPEC,
+ text(
+ "-Xmx2048m -XX:MaxPermSize=512m -Xms1024m",
+ "-Xmx2048m -XX:MaxPermSize=1024m -Xms1024m",
+ spec -> spec.path(JVM_CONFIG_PATH)
+ )
+ );
+ }
+}