From 6386121778d12b32b4d87d37d901703b9fcf5ce2 Mon Sep 17 00:00:00 2001 From: Marius Barbulescu Date: Fri, 10 Jul 2026 20:59:20 +0200 Subject: [PATCH 01/13] convert Spring Boot application-*.properties to application-*.yaml --- build.gradle.kts | 3 + .../java/spring/ConvertPropertiesToYaml.java | 485 ++++++++ .../resources/META-INF/rewrite/recipes.csv | 29 +- .../spring/ConvertPropertiesToYamlTest.java | 1064 +++++++++++++++++ 4 files changed, 1567 insertions(+), 14 deletions(-) create mode 100644 src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java create mode 100644 src/test/java/org/openrewrite/java/spring/ConvertPropertiesToYamlTest.java diff --git a/build.gradle.kts b/build.gradle.kts index 08bc33834..cb3ed2797 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -210,6 +210,9 @@ dependencies { implementation("org.openrewrite.recipe:rewrite-static-analysis:${rewriteVersion}") implementation("org.openrewrite.gradle.tooling:model:${rewriteVersion}") + // Provided at runtime by rewrite-yaml; compileOnly avoids pinning a second version + compileOnly("org.yaml:snakeyaml:2.6") + runtimeOnly("org.openrewrite:rewrite-java-21") runtimeOnly("org.openrewrite.recipe:rewrite-apache:$rewriteVersion") runtimeOnly("org.openrewrite.recipe:rewrite-hibernate:$rewriteVersion") diff --git a/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java b/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java new file mode 100644 index 000000000..5e1157d22 --- /dev/null +++ b/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java @@ -0,0 +1,485 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * 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.java.spring; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.jspecify.annotations.Nullable; +import org.openrewrite.*; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaSourceFile; +import org.openrewrite.marker.SearchResult; +import org.openrewrite.properties.tree.Properties; +import org.openrewrite.yaml.YamlParser; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.SafeConstructor; +import org.yaml.snakeyaml.nodes.NodeId; +import org.yaml.snakeyaml.nodes.Tag; +import org.yaml.snakeyaml.resolver.Resolver; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.io.UncheckedIOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.util.Collections.emptyList; + +@Value +@EqualsAndHashCode(callSuper = false) +public class ConvertPropertiesToYaml extends ScanningRecipe { + + private static final Pattern PROPERTIES_NAME_PATTERN = Pattern.compile("application(-.+)?\\.properties"); + private static final Pattern YAML_NAME_PATTERN = Pattern.compile("application(-.+)?\\.(yml|yaml)"); + // Matches candidate file names inside larger strings such as "classpath:application-dev.properties" + private static final Pattern REFERENCED_FILE_NAME_PATTERN = Pattern.compile("application(-[^/\\\\:\\s]+)?\\.properties"); + private static final String YAML_SPECIAL_CHARS = ":#[]{}|>&*!'\"%@`"; + private static final Resolver YAML_RESOLVER = new Resolver(); + // Splits a key on its first index: base, index, rest (e.g. my.servers[0].host → my.servers, 0, .host) + private static final Pattern INDEXED_KEY_PATTERN = Pattern.compile("(.+?)\\[(\\d+)](.*)"); + + @Option(displayName = "File extension", + description = "The extension to use for the generated YAML files. Defaults to `yaml`.", + valid = {"yaml", "yml"}, + example = "yml", + required = false) + @Nullable + String fileExtension; + + @Override + public String getDisplayName() { + return "Convert Spring `application-*.properties` to `application-*.yaml`"; + } + + @Override + public String getDescription() { + return "Converts Spring Boot `application-*.properties` files to `application-*.yaml`. " + + "The original `.properties` file is deleted and its comments are carried over. " + + "Conversion is skipped (with a message) " + + "when a corresponding `.yml` or `.yaml` file already exists."; + } + + public static class Accumulator { + final Map toConvert = new LinkedHashMap<>(); + // parent directory -> file name stem (e.g. "application-dev") -> existing .yml/.yaml file + final Map> existingYaml = new HashMap<>(); + final Set fileNamesReferencedFromJava = new HashSet<>(); + } + + @Override + public Accumulator getInitialValue(ExecutionContext ctx) { + return new Accumulator(); + } + + @Override + public TreeVisitor getScanner(Accumulator acc) { + TreeVisitor isSpringConfigFile = new IsPossibleSpringConfigFile(); + return new TreeVisitor() { + @Override + public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) { + if (!(tree instanceof SourceFile)) { + return tree; + } + SourceFile source = (SourceFile) tree; + Path sourcePath = source.getSourcePath(); + String fileName = sourcePath.getFileName() == null ? "" : sourcePath.getFileName().toString(); + + // Track existing YAML files by path, regardless of how they were parsed + // (a sibling .yml that failed to parse as YAML must still prevent conversion) + if (YAML_NAME_PATTERN.matcher(fileName).matches()) { + String stem = fileName.replaceAll("\\.(yml|yaml)$", ""); + Path parent = sourcePath.getParent() != null ? sourcePath.getParent() : Paths.get(""); + acc.existingYaml.computeIfAbsent(parent, k -> new HashMap<>()).putIfAbsent(stem, sourcePath); + return tree; + } + + // Track properties files that are candidates for conversion + if (tree instanceof Properties.File && + PROPERTIES_NAME_PATTERN.matcher(fileName).matches() && + isSpringConfigFile.visit(tree, ctx) != tree) { + acc.toConvert.put(sourcePath, source); + } + + // Track file names referenced from Java string literals (e.g. @PropertySource, + // @TestPropertySource, resource loading): such references cannot load YAML, + // so the referenced files must not be converted + if (tree instanceof JavaSourceFile) { + new JavaIsoVisitor() { + @Override + public J.Literal visitLiteral(J.Literal literal, ExecutionContext ctx) { + if (literal.getValue() instanceof String) { + Matcher reference = REFERENCED_FILE_NAME_PATTERN.matcher((String) literal.getValue()); + while (reference.find()) { + acc.fileNamesReferencedFromJava.add(reference.group()); + } + } + return literal; + } + }.visit(tree, ctx); + } + + return tree; + } + }; + } + + @Override + public Collection generate(Accumulator acc, ExecutionContext ctx) { + if (acc.toConvert.isEmpty()) { + return emptyList(); + } + List newFiles = new ArrayList<>(); + acc.toConvert.forEach((propertiesPath, value) -> { + if (findExistingYaml(acc, propertiesPath) != null || + acc.fileNamesReferencedFromJava.contains(propertiesPath.getFileName().toString())) { + return; + } + String yamlContent = buildYamlContent((Properties.File) value); + if (yamlContent.isEmpty()) { + return; + } + YamlParser.builder().build() + .parse(yamlContent) + .findFirst() + .map(brandNew -> (SourceFile) brandNew + .withSourcePath(toYamlPath(propertiesPath)) + // Copy markers (SourceSet, JavaProject, …) from the source file + // so the generated file is placed in the correct source set / module + .withMarkers(value.getMarkers())) + .ifPresent(newFiles::add); + }); + return newFiles; + } + + @Override + public TreeVisitor getVisitor(Accumulator acc) { + return new TreeVisitor() { + @Override + public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) { + if (!(tree instanceof Properties.File)) { + return tree; + } + Properties.File propertiesFile = (Properties.File) tree; + Path sourcePath = propertiesFile.getSourcePath(); + if (!acc.toConvert.containsKey(sourcePath)) { + return tree; + } + + if (acc.fileNamesReferencedFromJava.contains(sourcePath.getFileName().toString())) { + return SearchResult.found( + propertiesFile, + "Skipped: this file is referenced from Java sources (e.g. `@PropertySource`), " + + "which cannot load YAML files. Update those references before converting." + ); + } + + Path conflictingYaml = findExistingYaml(acc, sourcePath); + if (conflictingYaml != null) { + // Attach a visible skip message instead of deleting + return SearchResult.found( + propertiesFile, + "Skipped: a corresponding YAML file already exists at '" + conflictingYaml + "'. " + + "Merge these properties into it manually; when both files exist the " + + "`.properties` values take precedence, so converting automatically " + + "could change the effective configuration." + ); + } + // Delete the original .properties file + return null; + } + }; + } + + private @Nullable Path findExistingYaml(Accumulator acc, Path propertiesPath) { + Path parent = propertiesPath.getParent() != null ? propertiesPath.getParent() : Paths.get(""); + String stem = propertiesPath.getFileName().toString().replaceAll("\\.properties$", ""); + Map stems = acc.existingYaml.get(parent); + return stems == null ? null : stems.get(stem); + } + + private Path toYamlPath(Path propertiesPath) { + String extension = fileExtension == null ? "yaml" : fileExtension; + String newName = propertiesPath.getFileName().toString().replaceAll("\\.properties$", "." + extension); + return propertiesPath.resolveSibling(newName); + } + + /** + * An entry with key and value already unescaped, plus the comment lines that preceded it. + */ + @Value + private static class KeyValue { + String key; + String value; + List comments; + } + + private String buildYamlContent(Properties.File file) { + List entries = new ArrayList<>(); + List pendingComments = new ArrayList<>(); + for (Properties.Content content : file.getContent()) { + if (content instanceof Properties.Comment) { + pendingComments.add(((Properties.Comment) content).getMessage()); + } else if (content instanceof Properties.Entry) { + entries.add(toKeyValue((Properties.Entry) content, pendingComments)); + pendingComments = new ArrayList<>(); + } + } + List lines = renderMap(buildTree(entries), 0); + for (String comment : pendingComments) { + lines.add("#" + comment); + } + return lines.isEmpty() ? "" : String.join("\n", lines) + "\n"; + } + + /** + * Round-trips the raw entry through {@link java.util.Properties#load(Reader)} so key and + * value are unescaped exactly as Spring would see them at runtime. + */ + private KeyValue toKeyValue(Properties.Entry entry, List comments) { + java.util.Properties loaded = new java.util.Properties(); + try { + loaded.load(new StringReader(entry.getKey() + "=" + entry.getValue().getText())); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + String key = loaded.stringPropertyNames().iterator().next(); + return new KeyValue(key, loaded.getProperty(key), comments); + } + + /** + * Builds a nested structure of mappings (in first-seen key order), sequences and scalars. + * A key that would nest inside another key's value (e.g. {@code a.b.c=2} alongside + * {@code a.b=1}) keeps its conflicting remainder as a literal dotted key, which Spring's + * relaxed binding reads the same way. + */ + private Map buildTree(List entries) { + Map>> sequences = groupSequences(entries); + Set terminalKeys = new HashSet<>(); + for (KeyValue entry : entries) { + Matcher indexed = INDEXED_KEY_PATTERN.matcher(entry.getKey()); + terminalKeys.add(indexed.matches() && sequences.containsKey(indexed.group(1)) ? + indexed.group(1) : entry.getKey()); + } + Map root = new LinkedHashMap<>(); + Set insertedSequences = new HashSet<>(); + for (KeyValue entry : entries) { + Matcher indexed = INDEXED_KEY_PATTERN.matcher(entry.getKey()); + if (indexed.matches() && sequences.containsKey(indexed.group(1))) { + if (insertedSequences.add(indexed.group(1))) { + insert(root, indexed.group(1), buildSequence(sequences.get(indexed.group(1))), terminalKeys); + } + continue; + } + insert(root, entry.getKey(), entry, terminalKeys); + } + return root; + } + + /** + * A sequence item is either a scalar (single entry with an empty key) or a nested mapping. + */ + private List buildSequence(NavigableMap> items) { + List sequence = new ArrayList<>(); + for (List item : items.values()) { + sequence.add(item.get(0).getKey().isEmpty() ? item.get(0) : buildTree(item)); + } + return sequence; + } + + /** + * Never creates a mapping at a path that is itself a terminal key: from that point on the + * remainder stays a literal dotted key, so a scalar (or sequence) and its dotted descendants + * can coexist without duplicate YAML keys, regardless of entry order. + */ + @SuppressWarnings("unchecked") + private void insert(Map root, String key, Object value, Set terminalKeys) { + Map current = root; + String[] segments = key.split("\\."); + StringBuilder path = new StringBuilder(); + for (int i = 0; i < segments.length - 1; i++) { + path.append(i == 0 ? "" : ".").append(segments[i]); + if (terminalKeys.contains(path.toString())) { + current.put(String.join(".", Arrays.asList(segments).subList(i, segments.length)), value); + return; + } + current = (Map) current.computeIfAbsent(segments[i], k -> new LinkedHashMap<>()); + } + current.put(segments[segments.length - 1], value); + } + + @SuppressWarnings("unchecked") + private List renderMap(Map map, int depth) { + List lines = new ArrayList<>(); + String indent = indent(depth); + map.forEach((key, value) -> { + if (value instanceof KeyValue) { + KeyValue kv = (KeyValue) value; + addComments(lines, indent, kv); + lines.add(indent + key + ": " + quoteYamlValue(kv.getValue())); + } else if (value instanceof Map) { + lines.add(indent + key + ":"); + lines.addAll(renderMap((Map) value, depth + 1)); + } else { + lines.add(indent + key + ":"); + lines.addAll(renderSequence((List) value, depth + 1)); + } + }); + return lines; + } + + @SuppressWarnings("unchecked") + private List renderSequence(List sequence, int depth) { + List lines = new ArrayList<>(); + String indent = indent(depth); + for (Object item : sequence) { + if (item instanceof KeyValue) { + KeyValue kv = (KeyValue) item; + addComments(lines, indent, kv); + lines.add(indent + "- " + quoteYamlValue(kv.getValue())); + } else { + // Hang the mapping's first line off the dash, hoisting any comments above it, + // and align the remaining lines with the first + List itemLines = renderMap((Map) item, 0); + int first = 0; + while (itemLines.get(first).startsWith("#")) { + lines.add(indent + itemLines.get(first++)); + } + lines.add(indent + "- " + itemLines.get(first)); + for (int i = first + 1; i < itemLines.size(); i++) { + lines.add(indent + " " + itemLines.get(i)); + } + } + } + return lines; + } + + private void addComments(List lines, String indent, KeyValue kv) { + for (String comment : kv.getComments()) { + lines.add(indent + "#" + comment); + } + } + + private String indent(int depth) { + StringBuilder sb = new StringBuilder(depth * 2); + for (int i = 0; i < depth; i++) { + sb.append(" "); + } + return sb.toString(); + } + + /** + * Groups indexed keys by the base key before their first index (e.g. {@code my.list[0]} and + * {@code my.servers[0].host} group under {@code my.list} / {@code my.servers}), keeping only + * groups that can be faithfully represented as a YAML sequence: indices must be exactly + * {@code 0..n-1}, each index must be either a single scalar or a set of object keys (not both), + * and the base key must not also be used as a plain key. Directly nested indices + * ({@code a[0][1]}) are not converted. + */ + private Map>> groupSequences(List entries) { + Map>> sequences = new LinkedHashMap<>(); + Set invalid = new HashSet<>(); + Set plainKeys = new HashSet<>(); + for (KeyValue entry : entries) { + Matcher indexed = INDEXED_KEY_PATTERN.matcher(entry.getKey()); + if (!indexed.matches()) { + plainKeys.add(entry.getKey()); + continue; + } + String base = indexed.group(1); + int index = Integer.parseInt(indexed.group(2)); + String rest = indexed.group(3); + if (rest.startsWith("[")) { + invalid.add(base); + continue; + } + String itemKey = rest.startsWith(".") ? rest.substring(1) : rest; + sequences.computeIfAbsent(base, k -> new TreeMap<>()) + .computeIfAbsent(index, k -> new ArrayList<>()) + .add(new KeyValue(itemKey, entry.getValue(), entry.getComments())); + } + sequences.entrySet().removeIf(e -> invalid.contains(e.getKey()) || + plainKeys.contains(e.getKey()) || + e.getValue().firstKey() != 0 || + e.getValue().lastKey() != e.getValue().size() - 1 || + e.getValue().values().stream().anyMatch(this::isInvalidSequenceItem)); + return sequences; + } + + /** + * A sequence item must be either exactly one scalar (empty item key) or one or more object keys. + */ + private boolean isInvalidSequenceItem(List item) { + boolean scalar = item.get(0).getKey().isEmpty(); + if (scalar) { + return item.size() > 1; + } + return item.stream().anyMatch(kv -> kv.getKey().isEmpty()); + } + + /** + * Quotes a scalar when leaving it plain would change its meaning: YAML special or control + * characters, leading/trailing whitespace, block indicators, or re-typed values. + */ + private String quoteYamlValue(String value) { + if (value.isEmpty()) { + return "\"\""; + } + boolean needsQuoting = false; + for (char c : value.toCharArray()) { + if (c < ' ' || YAML_SPECIAL_CHARS.indexOf(c) >= 0) { + needsQuoting = true; + break; + } + } + needsQuoting = needsQuoting || + Character.isWhitespace(value.charAt(0)) || + Character.isWhitespace(value.charAt(value.length() - 1)) || + value.startsWith("- ") || "-".equals(value) || + value.startsWith("? ") || "?".equals(value) || + typeChangesWhenPlain(value); + if (!needsQuoting) { + return value; + } + String escaped = value + .replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\t", "\\t") + .replace("\r", "\\r") + .replace("\f", "\\f"); + return "\"" + escaped + "\""; + } + + /** + * True when YAML 1.1 resolves the plain scalar to a non-string type whose rendering differs + * from the original text (e.g. {@code on} → {@code true}, {@code 0x1A} → {@code 26}, + * {@code 2001-12-14} → a timestamp), which would change the value Spring binds. + * Values that round-trip textually (e.g. {@code 8080}, {@code true}, {@code 1.5}) stay plain. + */ + private boolean typeChangesWhenPlain(String value) { + if (YAML_RESOLVER.resolve(NodeId.scalar, value, true) == Tag.STR) { + return false; + } + Object loaded = new Yaml(new SafeConstructor(new LoaderOptions())).load(value); + return loaded == null || !value.equals(loaded.toString()); + } +} diff --git a/src/main/resources/META-INF/rewrite/recipes.csv b/src/main/resources/META-INF/rewrite/recipes.csv index b56f6eea1..085dc2d4d 100644 --- a/src/main/resources/META-INF/rewrite/recipes.csv +++ b/src/main/resources/META-INF/rewrite/recipes.csv @@ -10,6 +10,7 @@ maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.ChangeMe maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.ChangeSpringPropertyKey,Change the key of a Spring application property,"Change Spring application property keys existing in either Properties or YAML files, and in `@Value`, `@ConditionalOnProperty` or `@SpringBootTest` annotations.",1,,,,,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,"[{""name"":""oldPropertyKey"",""type"":""String"",""displayName"":""Old property key"",""description"":""The property key to rename."",""example"":""management.metrics.binders.*.enabled"",""required"":true},{""name"":""newPropertyKey"",""type"":""String"",""displayName"":""New property key"",""description"":""The new name for the property key."",""example"":""management.metrics.enable.process.files"",""required"":true},{""name"":""except"",""type"":""List"",""displayName"":""Except"",""description"":""Regex. If any of these property keys exist as direct children of `oldPropertyKey`, then they will not be moved to `newPropertyKey`."",""example"":""jvm""}]", maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.ChangeSpringPropertyValue,Change the value of a spring application property,"Change Spring application property values existing in either Properties or YAML files, and in `@Value`, `@ConditionalOnProperty`, `@SpringBootTest`, or `@TestPropertySource` annotations.",1,,,,,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,"[{""name"":""propertyKey"",""type"":""String"",""displayName"":""Property key"",""description"":""The name of the property key whose value is to be changed."",""example"":""management.metrics.binders.files.enabled"",""required"":true},{""name"":""newValue"",""type"":""String"",""displayName"":""New value"",""description"":""The new value to be used for key specified by `propertyKey`."",""example"":""management.metrics.enable.process.files"",""required"":true},{""name"":""oldValue"",""type"":""String"",""displayName"":""Old value"",""description"":""Only change the property value if it matches the configured `oldValue`."",""example"":""false""},{""name"":""regex"",""type"":""Boolean"",""displayName"":""Regex"",""description"":""Default false. If enabled, `oldValue` will be interpreted as a Regular Expression, and capture group contents will be available in `newValue`""},{""name"":""relaxedBinding"",""type"":""Boolean"",""displayName"":""Use relaxed binding"",""description"":""Whether to match the `propertyKey` using [relaxed binding](https://docs.spring.io/spring-boot/docs/2.5.6/reference/html/features.html#features.external-config.typesafe-configuration-properties.relaxed-binding) rules. Default is `true`. Set to `false` to use exact matching.""}]", maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.CommentOutSpringPropertyKey,Comment out Spring properties,"Add comment to specified Spring properties, and optionally comment out the property.",1,,,,,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,"[{""name"":""propertyKey"",""type"":""String"",""displayName"":""Property key"",""description"":""The name of the property key to comment out."",""example"":""management.metrics.binders.files.enabled"",""required"":true},{""name"":""comment"",""type"":""String"",""displayName"":""Comment"",""description"":""Comment to replace the property key."",""example"":""This property is deprecated and no longer applicable starting from Spring Boot 3.0.x"",""required"":true},{""name"":""commentOutProperty"",""type"":""Boolean"",""displayName"":""Comment out property"",""description"":""If `false` the property is kept and only the comment is added. Defaults to `true`.""}]", +maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.ConvertPropertiesToYaml,Convert Spring `application-*.properties` to `application-*.yaml`,Converts Spring Boot `application-*.properties` files to `application-*.yaml`. The original `.properties` file is deleted and its comments are carried over. Conversion is skipped (with a message) when a corresponding `.yml` or `.yaml` file already exists.,1,,,,,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,"[{""name"":""fileExtension"",""type"":""String"",""displayName"":""File extension"",""description"":""The extension to use for the generated YAML files. Defaults to `yaml`."",""example"":""yml"",""valid"":[""yaml"",""yml""]}]", maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.DeleteSpringProperty,Delete a spring configuration property,Delete a spring configuration property from any configuration file that contains a matching key.,1,,,,,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,"[{""name"":""propertyKey"",""type"":""String"",""displayName"":""Property key"",""description"":""The property key to delete. Supports glob expressions"",""example"":""management.endpoint.configprops.*"",""required"":true}]", maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.ExpandProperties,Expand Spring YAML properties,Expand YAML properties to not use the dot syntax shortcut.,1,,,,,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,"[{""name"":""sourceFileMask"",""type"":""String"",""displayName"":""Source file mask"",""description"":""An optional source file path mask use to restrict which YAML files will be expanded by this recipe."",""example"":""**/application*.yml""}]", maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.ImplicitWebAnnotationNames,Remove implicit web annotation names,Removes implicit web annotation names.,1,,,,,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, @@ -143,7 +144,7 @@ maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot3.Re maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot3.ReplaceRestTemplateBuilderMethods,Replace deprecated setters in `RestTemplateBuilder`,"Replaces `setConnectTimeout`, `setReadTimeout`, and `setSslBundle` method invocations with `connectTimeout`, `readTimeout`, and `sslBundle` respectively.",5,,,,Spring Boot 3.x,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot3.ReplaceRestTemplateBuilderRequestFactoryMethod,Replace `RestTemplateBuilder.requestFactory(Function)` with `requestFactoryBuilder`,"`RestTemplateBuilder.requestFactory(java.util.function.Function)` was deprecated since Spring Boot 3.4, in favor of `requestFactoryBuilder(ClientHttpRequestFactoryBuilder)`.",1,,,,Spring Boot 3.x,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot3.ReplaceStringLiteralsWithConstants,Replace String literals with Spring constants,Replace String literals with Spring constants where applicable.,97,,,,Spring Boot 3.x,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, -maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot3.SpringBoot33BestPractices,Spring Boot 3.3 best practices,Applies best practices to Spring Boot 3 applications.,3171,,,,Spring Boot 3.x,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" +maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot3.SpringBoot33BestPractices,Spring Boot 3.3 best practices,Applies best practices to Spring Boot 3 applications.,3175,,,,Spring Boot 3.x,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot3.SpringBoot3BestPracticesOnly,Spring Boot 3.3 best practices (only),"Applies best practices to Spring Boot 3 applications, without chaining in upgrades to Spring Boot.",109,,,,Spring Boot 3.x,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot3.SpringBootProperties_3_0,Migrate Spring Boot properties to 3.0,Migrate properties found in `application.properties` and `application.yml`.,284,,,,Spring Boot 3.x,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot3.SpringBootProperties_3_1,Migrate Spring Boot properties to 3.1,Migrate properties found in `application.properties` and `application.yml`.,7,,,,Spring Boot 3.x,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, @@ -163,23 +164,23 @@ maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot3.Up maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot3.UpgradeMyBatisToSpringBoot_2_7,Upgrade MyBatis to Spring Boot 2.7,Upgrade MyBatis Spring modules to a version corresponding to Spring Boot 2.7.,16,,,,Spring Boot 3.x,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot3.UpgradeMyBatisToSpringBoot_3_0,Upgrade MyBatis to Spring Boot 3.0,Upgrade MyBatis Spring modules to a version corresponding to Spring Boot 3.0.,18,,,,Spring Boot 3.x,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot3.UpgradeMyBatisToSpringBoot_3_2,Upgrade MyBatis to Spring Boot 3.2,Upgrade MyBatis Spring modules to a version corresponding to Spring Boot 3.2.,20,,,,Spring Boot 3.x,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, -maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_0,Migrate to Spring Boot 3.0,"Migrate applications to the latest Spring Boot 3.0 release. This recipe will modify an application's build files, make changes to deprecated/preferred APIs, and migrate configuration settings that have changes between versions. This recipe will also chain additional framework migrations (Spring Framework, Spring Data, etc) that are required as part of the migration to Spring Boot 2.7.",2990,,,,Spring Boot 3.x,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" -maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_1,Migrate to Spring Boot 3.1,"Migrate applications to the latest Spring Boot 3.1 release. This recipe will modify an application's build files, make changes to deprecated/preferred APIs, and migrate configuration settings that have changes between versions. This recipe will also chain additional framework migrations (Spring Framework, Spring Data, etc) that are required as part of the migration to Spring Boot 3.0.",3049,,,,Spring Boot 3.x,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" -maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_2,Migrate to Spring Boot 3.2,"Migrate applications to the latest Spring Boot 3.2 release. This recipe will modify an application's build files, make changes to deprecated/preferred APIs, and migrate configuration settings that have changes between versions. This recipe will also chain additional framework migrations (Spring Framework, Spring Data, etc) that are required as part of the migration to Spring Boot 3.1.",3123,,,,Spring Boot 3.x,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" -maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_3,Migrate to Spring Boot 3.3,"Migrate applications to the latest Spring Boot 3.3 release. This recipe will modify an application's build files, make changes to deprecated/preferred APIs, and migrate configuration settings that have changes between versions. This recipe will also chain additional framework migrations (Spring Framework, Spring Data, etc) that are required as part of the migration to Spring Boot 3.2.",3170,,,,Spring Boot 3.x,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" -maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_4,Migrate to Spring Boot 3.4,"Migrate applications to the latest Spring Boot 3.4 release. This recipe will modify an application's build files, make changes to deprecated/preferred APIs.",3307,,,,Spring Boot 3.x,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" -maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_5,Migrate to Spring Boot 3.5,"Migrate applications to the latest Spring Boot 3.5 release. This recipe will modify an application's build files, make changes to deprecated/preferred APIs.",3377,,,,Spring Boot 3.x,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" +maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_0,Migrate to Spring Boot 3.0,"Migrate applications to the latest Spring Boot 3.0 release. This recipe will modify an application's build files, make changes to deprecated/preferred APIs, and migrate configuration settings that have changes between versions. This recipe will also chain additional framework migrations (Spring Framework, Spring Data, etc) that are required as part of the migration to Spring Boot 2.7.",2994,,,,Spring Boot 3.x,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" +maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_1,Migrate to Spring Boot 3.1,"Migrate applications to the latest Spring Boot 3.1 release. This recipe will modify an application's build files, make changes to deprecated/preferred APIs, and migrate configuration settings that have changes between versions. This recipe will also chain additional framework migrations (Spring Framework, Spring Data, etc) that are required as part of the migration to Spring Boot 3.0.",3053,,,,Spring Boot 3.x,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" +maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_2,Migrate to Spring Boot 3.2,"Migrate applications to the latest Spring Boot 3.2 release. This recipe will modify an application's build files, make changes to deprecated/preferred APIs, and migrate configuration settings that have changes between versions. This recipe will also chain additional framework migrations (Spring Framework, Spring Data, etc) that are required as part of the migration to Spring Boot 3.1.",3127,,,,Spring Boot 3.x,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" +maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_3,Migrate to Spring Boot 3.3,"Migrate applications to the latest Spring Boot 3.3 release. This recipe will modify an application's build files, make changes to deprecated/preferred APIs, and migrate configuration settings that have changes between versions. This recipe will also chain additional framework migrations (Spring Framework, Spring Data, etc) that are required as part of the migration to Spring Boot 3.2.",3174,,,,Spring Boot 3.x,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" +maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_4,Migrate to Spring Boot 3.4,"Migrate applications to the latest Spring Boot 3.4 release. This recipe will modify an application's build files, make changes to deprecated/preferred APIs.",3311,,,,Spring Boot 3.x,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" +maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_5,Migrate to Spring Boot 3.5,"Migrate applications to the latest Spring Boot 3.5 release. This recipe will modify an application's build files, make changes to deprecated/preferred APIs.",3381,,,,Spring Boot 3.x,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot4.AddAutoConfigureTestRestTemplate,Add `@AutoConfigureTestRestTemplate` if necessary,Adds `@AutoConfigureTestRestTemplate` to test classes annotated with `@SpringBootTest` that use `TestRestTemplate` since this bean is no longer auto-configured as described in the [Spring Boot 4 migration guide](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide#using-webclient-or-testresttemplate-and-springboottest).,1,,,,Boot4,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot4.AddAutoConfigureWebTestClient,Add `@AutoConfigureWebTestClient` if necessary,Adds `@AutoConfigureWebTestClient` to test classes annotated with `@SpringBootTest` that use `WebTestClient` since this bean is no longer auto-configured as described in the [Spring Boot 4 migration guide](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide#using-webclient-or-testresttemplate-and-springboottest).,1,,,,Boot4,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot4.AddSpringBootStarterFlyway,Add `spring-boot-starter-flyway` if using Flyway,Adds the necessary Spring Boot 4.0 Flyway starter for autoconfiguration based on dependency usage.,2,,,,Boot4,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, -maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot4.MigrateAutoconfigurePackages,Migrate packages to modular starters,Migrate to new packages used for autoconfiguration by Spring Boot 4.0 modules.,86,,,,Boot4,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, +maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot4.MigrateAutoconfigurePackages,Migrate packages to modular starters,Migrate to new packages used for autoconfiguration by Spring Boot 4.0 modules.,87,,,,Boot4,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot4.MigrateToModularStarters,Migrate to Spring Boot 4.0 modular starters,"Adds the necessary Spring Boot 4.0 starter dependencies based on package usage. Spring Boot 4.0 has a modular design requiring explicit starters for each feature. This recipe detects feature usage via package imports and adds the appropriate starters. -Note: Higher-level starters (like data-jpa) include lower-level ones (like jdbc) transitively, so only the highest-level detected starter is added for each technology.",102,,,,Boot4,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, +Note: Higher-level starters (like data-jpa) include lower-level ones (like jdbc) transitively, so only the highest-level detected starter is added for each technology.",103,,,,Boot4,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot4.RenameDeprecatedStartersManagedVersions,Rename Spring Boot 4.0 starters with managed versions,"Renames deprecated Spring Boot starters to their new names without adding explicit versions, for use in projects where the `io.spring.dependency-management` plugin manages versions via BOM.",7,,,,Boot4,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot4.ReplaceMockBeanAndSpyBean,Replace `@MockBean` and `@SpyBean`,Replaces `@MockBean` and `@SpyBean` annotations with `@MockitoBean` and `@MockitoSpyBean`.,11,,,,Boot4,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot4.SpringBootProperties_4_0,Migrate Spring Boot properties to 4.0,Migrate properties found in `application.properties` and `application.yml`.,155,,,,Boot4,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot4.UnwrapMockAndSpyBeanContainers,Unwrap `@MockBeans` and `@SpyBeans` container annotations,Replaces class-level `@MockBeans` and `@SpyBeans` container annotations with a single class-level `@MockBean` or `@SpyBean` annotation with a merged `types` attribute for compatibility with `@MockitoBean`.,1,,,,Boot4,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, -maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot4.UpgradeSpringBoot_4_0,Migrate to Spring Boot 4.0,"Migrate applications to the latest Spring Boot 4.0 release. This recipe will modify an application's build files, make changes to deprecated/preferred APIs.",4305,,,,Boot4,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" +maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.boot4.UpgradeSpringBoot_4_0,Migrate to Spring Boot 4.0,"Migrate applications to the latest Spring Boot 4.0 release. This recipe will modify an application's build files, make changes to deprecated/preferred APIs.",4310,,,,Boot4,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.cloud2022.AddLoggingPatternLevelForSleuth,Add logging.pattern.level for traceId and spanId,"Add `logging.pattern.level` for traceId and spanId which was previously set by default, if not already set.",1,,,,Spring Cloud 2022,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.cloud2022.DependencyUpgrades,Upgrade dependencies to Spring Cloud 2022,Upgrade dependencies to Spring Cloud 2022 from prior 2021.x version.,11,,,,Spring Cloud 2022,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.cloud2022.MigrateCloudSleuthToMicrometerTracing,Migrate Spring Cloud Sleuth 3.1 to Micrometer Tracing 1.0,Spring Cloud Sleuth has been discontinued and only compatible with Spring Boot 2.x.,29,,,,Spring Cloud 2022,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, @@ -245,10 +246,10 @@ maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.framewor maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.framework.UpgradeSpringFramework_5_1,Migrate to Spring Framework 5.1,Migrate applications to the latest Spring Framework 5.1 release.,15,,,,Spring Framework,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.framework.UpgradeSpringFramework_5_2,Migrate to Spring Framework 5.2,Migrate applications to the latest Spring Framework 5.2 release.,19,,,,Spring Framework,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.framework.UpgradeSpringFramework_5_3,Migrate to Spring Framework 5.3,Migrate applications to the latest Spring Framework 5.3 release.,30,,,,Spring Framework,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, -maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.framework.UpgradeSpringFramework_6_0,Migrate to Spring Framework 6.0,Migrate applications to the latest Spring Framework 6.0 release.,940,,,,Spring Framework,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" -maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.framework.UpgradeSpringFramework_6_1,Migrate to Spring Framework 6.1,Migrate applications to the latest Spring Framework 6.1 release.,943,,,,Spring Framework,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" -maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.framework.UpgradeSpringFramework_6_2,Migrate to Spring Framework 6.2,Migrate applications to the latest Spring Framework 6.2 release.,960,,,,Spring Framework,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" -maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.framework.UpgradeSpringFramework_7_0,Migrate to Spring Framework 7.0,Migrate applications to the latest Spring Framework 7.0 release.,1315,,,,Spring Framework,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" +maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.framework.UpgradeSpringFramework_6_0,Migrate to Spring Framework 6.0,Migrate applications to the latest Spring Framework 6.0 release.,944,,,,Spring Framework,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" +maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.framework.UpgradeSpringFramework_6_1,Migrate to Spring Framework 6.1,Migrate applications to the latest Spring Framework 6.1 release.,947,,,,Spring Framework,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" +maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.framework.UpgradeSpringFramework_6_2,Migrate to Spring Framework 6.2,Migrate applications to the latest Spring Framework 6.2 release.,964,,,,Spring Framework,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" +maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.framework.UpgradeSpringFramework_7_0,Migrate to Spring Framework 7.0,Migrate applications to the latest Spring Framework 7.0 release.,1319,,,,Spring Framework,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,,"[{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]" maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.framework.UseObjectUtilsIsEmpty,Use `ObjectUtils#isEmpty(Object)`,`StringUtils#isEmpty(Object)` was deprecated in 5.3.,1,,,,Spring Framework,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.http.ReplaceStringLiteralsWithHttpHeadersConstants,Replace String literals with `HttpHeaders` constants,Replace String literals with `org.springframework.http.HttpHeaders` constants.,62,,,,Http,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.http.ReplaceStringLiteralsWithMediaTypeConstants,Replace String literals with `MediaType` constants,Replace String literals with `org.springframework.http.MediaType` constants.,32,,,,Http,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, diff --git a/src/test/java/org/openrewrite/java/spring/ConvertPropertiesToYamlTest.java b/src/test/java/org/openrewrite/java/spring/ConvertPropertiesToYamlTest.java new file mode 100644 index 000000000..7d1e3e89a --- /dev/null +++ b/src/test/java/org/openrewrite/java/spring/ConvertPropertiesToYamlTest.java @@ -0,0 +1,1064 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * 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.java.spring; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.Tree; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; +import static org.openrewrite.java.Assertions.mavenProject; +import static org.openrewrite.java.Assertions.srcMainJava; +import static org.openrewrite.java.Assertions.srcMainResources; +import static org.openrewrite.java.Assertions.srcTestResources; +import static org.openrewrite.properties.Assertions.properties; +import static org.openrewrite.test.SourceSpecs.text; +import static org.openrewrite.yaml.Assertions.yaml; + +class ConvertPropertiesToYamlTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new ConvertPropertiesToYaml(null)); + } + + private static String skipMessage(String conflictingPath) { + return "~~(Skipped: a corresponding YAML file already exists at '" + conflictingPath + "'. " + + "Merge these properties into it manually; when both files exist the `.properties` values " + + "take precedence, so converting automatically could change the effective configuration.)~~>"; + } + + @DocumentExample + @Test + void singlePropertyIsConvertedToNestedYaml() { + rewriteRun( + mavenProject("project", + srcMainResources( + properties( + "server.port=8080", + null, + spec -> spec.path("application.properties") + ), + yaml( + doesNotExist(), + //language=yaml + """ + server: + port: 8080 + """, + spec -> spec.path("application.yaml") + ) + ) + ) + ); + } + + @Test + void multiplePropertiesAreConvertedToNestedYaml() { + rewriteRun( + mavenProject("project", + srcMainResources( + properties( + //language=properties + """ + server.port=8080 + spring.application.name=myapp + """, + null, + spec -> spec.path("application.properties") + ), + yaml( + doesNotExist(), + //language=yaml + """ + server: + port: 8080 + spring: + application: + name: myapp + """, + spec -> spec.path("application.yaml") + ) + ) + ) + ); + } + + @Test + void emptyPropertiesFileIsDeletedWithoutCreatingYaml() { + rewriteRun( + mavenProject("project", + srcMainResources( + properties( + "", + null, + spec -> spec.path("application.properties") + ) + ) + ) + ); + } + + @Test + void ymlExtensionOptionGeneratesYmlFile() { + rewriteRun( + spec -> spec.recipe(new ConvertPropertiesToYaml("yml")), + mavenProject("project", + srcMainResources( + properties( + "server.port=8080", + null, + spec -> spec.path("application.properties") + ), + yaml( + doesNotExist(), + //language=yaml + """ + server: + port: 8080 + """, + spec -> spec.path("application.yml") + ) + ) + ) + ); + } + + @Test + void profileSpecificPropertiesFileIsConverted() { + rewriteRun( + mavenProject("project", + srcMainResources( + properties( + "spring.datasource.url=jdbc:h2:mem:devdb", + null, + spec -> spec.path("application-dev.properties") + ), + yaml( + doesNotExist(), + //language=yaml + """ + spring: + datasource: + url: "jdbc:h2:mem:devdb" + """, + spec -> spec.path("application-dev.yaml") + ) + ) + ) + ); + } + + @Test + void multipleProfileFilesAreConvertedIndependently() { + rewriteRun( + mavenProject("project", + srcMainResources( + properties( + "server.port=8081", + null, + spec -> spec.path("application-dev.properties") + ), + properties( + "server.port=8082", + null, + spec -> spec.path("application-prod.properties") + ), + yaml( + doesNotExist(), + //language=yaml + """ + server: + port: 8081 + """, + spec -> spec.path("application-dev.yaml") + ), + yaml( + doesNotExist(), + //language=yaml + """ + server: + port: 8082 + """, + spec -> spec.path("application-prod.yaml") + ) + ) + ) + ); + } + + @Test + void testResourcesAreAlsoConverted() { + rewriteRun( + mavenProject("project", + srcTestResources( + properties( + "server.port=9090", + null, + spec -> spec.path("application.properties") + ), + yaml( + doesNotExist(), + //language=yaml + """ + server: + port: 9090 + """, + spec -> spec.path("application.yaml") + ) + ) + ) + ); + } + + @Test + void mainAndTestResourcesAreBothConverted() { + rewriteRun( + mavenProject("project", + srcMainResources( + properties( + "server.port=8080", + null, + spec -> spec.path("application.properties") + ), + yaml( + doesNotExist(), + //language=yaml + """ + server: + port: 8080 + """, + spec -> spec.path("application.yaml") + ) + ), + srcTestResources( + properties( + "server.port=9090", + null, + spec -> spec.path("application.properties") + ), + yaml( + doesNotExist(), + //language=yaml + """ + server: + port: 9090 + """, + spec -> spec.path("application.yaml") + ) + ) + ) + ); + } + + @Test + void skipWithMessageWhenYmlAlreadyExists() { + rewriteRun( + mavenProject("project", + srcMainResources( + properties( + "server.port=8080", + skipMessage("project/src/main/resources/application.yml") + "server.port=8080", + spec -> spec.path("application.properties") + ), + yaml( + //language=yaml + "server:\n port: 9090", + spec -> spec.path("application.yml") + ) + ) + ) + ); + } + + @Test + void skipWithMessageWhenYamlAlreadyExists() { + rewriteRun( + mavenProject("project", + srcMainResources( + properties( + "server.port=8080", + skipMessage("project/src/main/resources/application.yaml") + "server.port=8080", + spec -> spec.path("application.properties") + ), + yaml( + //language=yaml + "server:\n port: 9090", + spec -> spec.path("application.yaml") + ) + ) + ) + ); + } + + @Test + void skipWithMessageForProfileFileWhenYamlAlreadyExists() { + rewriteRun( + mavenProject("project", + srcMainResources( + properties( + "server.port=8080", + skipMessage("project/src/main/resources/application-dev.yaml") + "server.port=8080", + spec -> spec.path("application-dev.properties") + ), + yaml( + //language=yaml + "server:\n port: 9090", + spec -> spec.path("application-dev.yaml") + ) + ) + ) + ); + } + + @Test + void skipWhenExistingYamlWasNotParsedAsYaml() { + rewriteRun( + mavenProject("project", + srcMainResources( + properties( + "server.port=8080", + skipMessage("project/src/main/resources/application.yml") + "server.port=8080", + spec -> spec.path("application.properties") + ), + text( + "server:\n port: 9090", + spec -> spec.path("application.yml") + ) + ) + ) + ); + } + + @Test + void skipWithMessageWhenReferencedFromJavaSources() { + rewriteRun( + mavenProject("project", + srcMainJava( + //language=java + java( + """ + class Config { + String location = "classpath:application.properties"; + } + """ + ) + ), + srcMainResources( + properties( + "server.port=8080", + "~~(Skipped: this file is referenced from Java sources (e.g. `@PropertySource`), " + + "which cannot load YAML files. Update those references before converting.)~~>server.port=8080", + spec -> spec.path("application.properties") + ) + ) + ) + ); + } + + @Test + void javaReferenceToOtherProfileDoesNotBlockConversion() { + rewriteRun( + mavenProject("project", + srcMainJava( + //language=java + java( + """ + class Config { + String location = "classpath:application-dev.properties"; + } + """ + ) + ), + srcMainResources( + properties( + "server.port=8080", + null, + spec -> spec.path("application.properties") + ), + yaml( + doesNotExist(), + //language=yaml + """ + server: + port: 8080 + """, + spec -> spec.path("application.yaml") + ) + ) + ) + ); + } + + @Test + void nonApplicationPropertiesFilesAreUntouched() { + rewriteRun( + mavenProject("project", + srcMainResources( + properties( + "greeting=hello", + spec -> spec.path("messages.properties") + ), + properties( + "bean.name=foo", + spec -> spec.path("applicationContext.properties") + ), + properties( + "greeting=hallo", + spec -> spec.path("application_de.properties") + ) + ) + ) + ); + } + + @Test + void propertiesFileOutsideAnySourceSetIsUntouched() { + rewriteRun( + properties( + "server.port=8080", + spec -> spec.path("application.properties") + ) + ); + } + + @Test + void propertiesFileMarkedAsSpringConfigIsConverted() { + rewriteRun( + properties( + "server.port=8080", + null, + spec -> spec.path("svc/config/application.properties") + .markers(new SpringConfigFile(Tree.randomId())) + ), + yaml( + doesNotExist(), + //language=yaml + """ + server: + port: 8080 + """, + spec -> spec.path("svc/config/application.yaml") + ) + ); + } + + @Test + void valueContainingColonIsQuoted() { + rewriteRun( + mavenProject("project", + srcMainResources( + properties( + "spring.datasource.url=jdbc:h2:mem:db", + null, + spec -> spec.path("application.properties") + ), + yaml( + doesNotExist(), + //language=yaml + """ + spring: + datasource: + url: "jdbc:h2:mem:db" + """, + spec -> spec.path("application.yaml") + ) + ) + ) + ); + } + + @Test + void valueContainingHashIsQuoted() { + rewriteRun( + mavenProject("project", + srcMainResources( + properties( + "app.message=hello # world", + null, + spec -> spec.path("application.properties") + ), + yaml( + doesNotExist(), + //language=yaml + """ + app: + message: "hello # world" + """, + spec -> spec.path("application.yaml") + ) + ) + ) + ); + } + + @Test + void emptyValueMapsToEmptyString() { + rewriteRun( + mavenProject("project", + srcMainResources( + properties( + "server.context-path=", + null, + spec -> spec.path("application.properties") + ), + yaml( + doesNotExist(), + //language=yaml + """ + server: + context-path: "" + """, + spec -> spec.path("application.yaml") + ) + ) + ) + ); + } + + @Test + void yamlBooleanLikeValuesAreQuotedToPreserveStringSemantics() { + rewriteRun( + mavenProject("project", + srcMainResources( + properties( + //language=properties + """ + app.a=yes + app.b=off + app.c=on + app.d=true + """, + null, + spec -> spec.path("application.properties") + ), + yaml( + doesNotExist(), + //language=yaml + """ + app: + a: "yes" + b: "off" + c: "on" + d: true + """, + spec -> spec.path("application.yaml") + ) + ) + ) + ); + } + + @Test + void yamlNullLikeValuesAreQuoted() { + rewriteRun( + mavenProject("project", + srcMainResources( + properties( + //language=properties + """ + app.a=null + app.b=~ + """, + null, + spec -> spec.path("application.properties") + ), + yaml( + doesNotExist(), + //language=yaml + """ + app: + a: "null" + b: "~" + """, + spec -> spec.path("application.yaml") + ) + ) + ) + ); + } + + @Test + void valuesReTypedByYamlAreQuotedOthersStayPlain() { + rewriteRun( + mavenProject("project", + srcMainResources( + properties( + //language=properties + """ + app.a=0x1A + app.b=1_000 + app.c=+1 + app.d=2001-12-14 + app.e=1.50 + app.f=8080 + app.g=1.5 + """, + null, + spec -> spec.path("application.properties") + ), + yaml( + doesNotExist(), + //language=yaml + """ + app: + a: "0x1A" + b: "1_000" + c: "+1" + d: "2001-12-14" + e: "1.50" + f: 8080 + g: 1.5 + """, + spec -> spec.path("application.yaml") + ) + ) + ) + ); + } + + @Test + void octalLikeValueIsQuoted() { + rewriteRun( + mavenProject("project", + srcMainResources( + properties( + "app.file-mask=0755", + null, + spec -> spec.path("application.properties") + ), + yaml( + doesNotExist(), + //language=yaml + """ + app: + file-mask: "0755" + """, + spec -> spec.path("application.yaml") + ) + ) + ) + ); + } + + @Test + void escapedNewlineAndTabAreTranslated() { + rewriteRun( + mavenProject("project", + srcMainResources( + properties( + "app.message=line1\\nline2\\tend", + null, + spec -> spec.path("application.properties") + ), + yaml( + doesNotExist(), + """ + app: + message: "line1\\nline2\\tend" + """, + spec -> spec.path("application.yaml") + ) + ) + ) + ); + } + + @Test + void escapedBackslashAndUnicodeAreTranslated() { + rewriteRun( + mavenProject("project", + srcMainResources( + properties( + "app.path=C:\\\\data\\u0021", + null, + spec -> spec.path("application.properties") + ), + yaml( + doesNotExist(), + """ + app: + path: "C:\\\\data!" + """, + spec -> spec.path("application.yaml") + ) + ) + ) + ); + } + + @Test + void escapedKeysAreUnescaped() { + rewriteRun( + mavenProject("project", + srcMainResources( + properties( + """ + my\\ key=1 + a\\:b=2 + """, + null, + spec -> spec.path("application.properties") + ), + yaml( + doesNotExist(), + """ + my key: 1 + a:b: 2 + """, + spec -> spec.path("application.yaml") + ) + ) + ) + ); + } + + @Test + void keyThatIsAlsoAPrefixOfOtherKeysStaysLiteral() { + rewriteRun( + mavenProject("project", + srcMainResources( + properties( + //language=properties + """ + a=1 + a.b=2 + b.c.d=3 + b.c=4 + """, + null, + spec -> spec.path("application.properties") + ), + yaml( + doesNotExist(), + //language=yaml + """ + a: 1 + a.b: 2 + b: + c.d: 3 + c: 4 + """, + spec -> spec.path("application.yaml") + ) + ) + ) + ); + } + + @Test + void commentLinesInPropertiesArePreserved() { + rewriteRun( + mavenProject("project", + srcMainResources( + properties( + //language=properties + """ + # header comment + server.port=8080 + ! also a comment + spring.application.name=myapp + # trailing comment + """, + null, + spec -> spec.path("application.properties") + ), + yaml( + doesNotExist(), + //language=yaml + """ + server: + # header comment + port: 8080 + spring: + application: + # also a comment + name: myapp + # trailing comment + """, + spec -> spec.path("application.yaml") + ) + ) + ) + ); + } + + @Test + void indexedKeysAreConvertedToYamlSequence() { + rewriteRun( + mavenProject("project", + srcMainResources( + properties( + //language=properties + """ + my.list[0]=a + other.key=x + my.list[1]=b + """, + null, + spec -> spec.path("application.properties") + ), + yaml( + doesNotExist(), + //language=yaml + """ + my: + list: + - a + - b + other: + key: x + """, + spec -> spec.path("application.yaml") + ) + ) + ) + ); + } + + @Test + void outOfOrderIndexedKeysAreSortedIntoSequence() { + rewriteRun( + mavenProject("project", + srcMainResources( + properties( + //language=properties + """ + my.list[1]=b + my.list[0]=a + """, + null, + spec -> spec.path("application.properties") + ), + yaml( + doesNotExist(), + //language=yaml + """ + my: + list: + - a + - b + """, + spec -> spec.path("application.yaml") + ) + ) + ) + ); + } + + @Test + void sequenceValuesAreQuotedWhenNeeded() { + rewriteRun( + mavenProject("project", + srcMainResources( + properties( + //language=properties + """ + app.urls[0]=jdbc:h2:mem:db + app.urls[1]=plain + """, + null, + spec -> spec.path("application.properties") + ), + yaml( + doesNotExist(), + //language=yaml + """ + app: + urls: + - "jdbc:h2:mem:db" + - plain + """, + spec -> spec.path("application.yaml") + ) + ) + ) + ); + } + + @Test + void nonContiguousIndexedKeysRemainFlat() { + rewriteRun( + mavenProject("project", + srcMainResources( + properties( + //language=properties + """ + my.list[0]=a + my.list[2]=c + """, + null, + spec -> spec.path("application.properties") + ), + yaml( + doesNotExist(), + //language=yaml + """ + my: + list[0]: a + list[2]: c + """, + spec -> spec.path("application.yaml") + ) + ) + ) + ); + } + + @Test + void objectListKeysAreConvertedToYamlSequence() { + rewriteRun( + mavenProject("project", + srcMainResources( + properties( + //language=properties + """ + my.servers[0].host=alpha + my.servers[0].port=8080 + my.servers[1].host=beta + my.servers[1].port=9090 + """, + null, + spec -> spec.path("application.properties") + ), + yaml( + doesNotExist(), + //language=yaml + """ + my: + servers: + - host: alpha + port: 8080 + - host: beta + port: 9090 + """, + spec -> spec.path("application.yaml") + ) + ) + ) + ); + } + + @Test + void nestedListInsideObjectListIsConverted() { + rewriteRun( + mavenProject("project", + srcMainResources( + properties( + //language=properties + """ + my.servers[0].host=alpha + my.servers[0].tags[0]=x + my.servers[0].tags[1]=y + """, + null, + spec -> spec.path("application.properties") + ), + yaml( + doesNotExist(), + //language=yaml + """ + my: + servers: + - host: alpha + tags: + - x + - y + """, + spec -> spec.path("application.yaml") + ) + ) + ) + ); + } + + @Test + void nonContiguousObjectListKeysRemainFlat() { + rewriteRun( + mavenProject("project", + srcMainResources( + properties( + //language=properties + """ + my.servers[0].host=alpha + my.servers[2].host=gamma + """, + null, + spec -> spec.path("application.properties") + ), + yaml( + doesNotExist(), + //language=yaml + """ + my: + servers[0]: + host: alpha + servers[2]: + host: gamma + """, + spec -> spec.path("application.yaml") + ) + ) + ) + ); + } + + @Test + void multiModuleProjectConvertsEachModuleIndependently() { + rewriteRun( + mavenProject("parent", + mavenProject("service", + srcMainResources( + properties( + "server.port=8081", + null, + spec -> spec.path("application.properties") + ), + yaml( + doesNotExist(), + //language=yaml + """ + server: + port: 8081 + """, + spec -> spec.path("application.yaml") + ) + ) + ), + mavenProject("client", + srcMainResources( + properties( + "server.port=8082", + null, + spec -> spec.path("application.properties") + ), + yaml( + doesNotExist(), + //language=yaml + """ + server: + port: 8082 + """, + spec -> spec.path("application.yaml") + ) + ) + ) + ) + ); + } +} From 0b664dfbf00aa348842a43a43461fb3eb2f38f8f Mon Sep 17 00:00:00 2001 From: Marius Barbulescu Date: Mon, 13 Jul 2026 23:45:16 +0200 Subject: [PATCH 02/13] extract properties to yaml conversion into a separate class --- .../java/spring/ConvertPropertiesToYaml.java | 277 +------- .../spring/PropertiesToYamlConverter.java | 310 +++++++++ .../spring/ConvertPropertiesToYamlTest.java | 591 ------------------ .../spring/PropertiesToYamlConverterTest.java | 354 +++++++++++ 4 files changed, 665 insertions(+), 867 deletions(-) create mode 100644 src/main/java/org/openrewrite/java/spring/PropertiesToYamlConverter.java create mode 100644 src/test/java/org/openrewrite/java/spring/PropertiesToYamlConverterTest.java diff --git a/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java b/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java index 5e1157d22..5adef576e 100644 --- a/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java +++ b/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java @@ -25,17 +25,7 @@ import org.openrewrite.marker.SearchResult; import org.openrewrite.properties.tree.Properties; import org.openrewrite.yaml.YamlParser; -import org.yaml.snakeyaml.LoaderOptions; -import org.yaml.snakeyaml.Yaml; -import org.yaml.snakeyaml.constructor.SafeConstructor; -import org.yaml.snakeyaml.nodes.NodeId; -import org.yaml.snakeyaml.nodes.Tag; -import org.yaml.snakeyaml.resolver.Resolver; -import java.io.IOException; -import java.io.Reader; -import java.io.StringReader; -import java.io.UncheckedIOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; @@ -52,10 +42,6 @@ public class ConvertPropertiesToYaml extends ScanningRecipe generate(Accumulator acc, ExecutionContext ctx) { acc.fileNamesReferencedFromJava.contains(propertiesPath.getFileName().toString())) { return; } - String yamlContent = buildYamlContent((Properties.File) value); + String yamlContent = PropertiesToYamlConverter.convert((Properties.File) value); if (yamlContent.isEmpty()) { return; } @@ -221,265 +207,4 @@ private Path toYamlPath(Path propertiesPath) { String newName = propertiesPath.getFileName().toString().replaceAll("\\.properties$", "." + extension); return propertiesPath.resolveSibling(newName); } - - /** - * An entry with key and value already unescaped, plus the comment lines that preceded it. - */ - @Value - private static class KeyValue { - String key; - String value; - List comments; - } - - private String buildYamlContent(Properties.File file) { - List entries = new ArrayList<>(); - List pendingComments = new ArrayList<>(); - for (Properties.Content content : file.getContent()) { - if (content instanceof Properties.Comment) { - pendingComments.add(((Properties.Comment) content).getMessage()); - } else if (content instanceof Properties.Entry) { - entries.add(toKeyValue((Properties.Entry) content, pendingComments)); - pendingComments = new ArrayList<>(); - } - } - List lines = renderMap(buildTree(entries), 0); - for (String comment : pendingComments) { - lines.add("#" + comment); - } - return lines.isEmpty() ? "" : String.join("\n", lines) + "\n"; - } - - /** - * Round-trips the raw entry through {@link java.util.Properties#load(Reader)} so key and - * value are unescaped exactly as Spring would see them at runtime. - */ - private KeyValue toKeyValue(Properties.Entry entry, List comments) { - java.util.Properties loaded = new java.util.Properties(); - try { - loaded.load(new StringReader(entry.getKey() + "=" + entry.getValue().getText())); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - String key = loaded.stringPropertyNames().iterator().next(); - return new KeyValue(key, loaded.getProperty(key), comments); - } - - /** - * Builds a nested structure of mappings (in first-seen key order), sequences and scalars. - * A key that would nest inside another key's value (e.g. {@code a.b.c=2} alongside - * {@code a.b=1}) keeps its conflicting remainder as a literal dotted key, which Spring's - * relaxed binding reads the same way. - */ - private Map buildTree(List entries) { - Map>> sequences = groupSequences(entries); - Set terminalKeys = new HashSet<>(); - for (KeyValue entry : entries) { - Matcher indexed = INDEXED_KEY_PATTERN.matcher(entry.getKey()); - terminalKeys.add(indexed.matches() && sequences.containsKey(indexed.group(1)) ? - indexed.group(1) : entry.getKey()); - } - Map root = new LinkedHashMap<>(); - Set insertedSequences = new HashSet<>(); - for (KeyValue entry : entries) { - Matcher indexed = INDEXED_KEY_PATTERN.matcher(entry.getKey()); - if (indexed.matches() && sequences.containsKey(indexed.group(1))) { - if (insertedSequences.add(indexed.group(1))) { - insert(root, indexed.group(1), buildSequence(sequences.get(indexed.group(1))), terminalKeys); - } - continue; - } - insert(root, entry.getKey(), entry, terminalKeys); - } - return root; - } - - /** - * A sequence item is either a scalar (single entry with an empty key) or a nested mapping. - */ - private List buildSequence(NavigableMap> items) { - List sequence = new ArrayList<>(); - for (List item : items.values()) { - sequence.add(item.get(0).getKey().isEmpty() ? item.get(0) : buildTree(item)); - } - return sequence; - } - - /** - * Never creates a mapping at a path that is itself a terminal key: from that point on the - * remainder stays a literal dotted key, so a scalar (or sequence) and its dotted descendants - * can coexist without duplicate YAML keys, regardless of entry order. - */ - @SuppressWarnings("unchecked") - private void insert(Map root, String key, Object value, Set terminalKeys) { - Map current = root; - String[] segments = key.split("\\."); - StringBuilder path = new StringBuilder(); - for (int i = 0; i < segments.length - 1; i++) { - path.append(i == 0 ? "" : ".").append(segments[i]); - if (terminalKeys.contains(path.toString())) { - current.put(String.join(".", Arrays.asList(segments).subList(i, segments.length)), value); - return; - } - current = (Map) current.computeIfAbsent(segments[i], k -> new LinkedHashMap<>()); - } - current.put(segments[segments.length - 1], value); - } - - @SuppressWarnings("unchecked") - private List renderMap(Map map, int depth) { - List lines = new ArrayList<>(); - String indent = indent(depth); - map.forEach((key, value) -> { - if (value instanceof KeyValue) { - KeyValue kv = (KeyValue) value; - addComments(lines, indent, kv); - lines.add(indent + key + ": " + quoteYamlValue(kv.getValue())); - } else if (value instanceof Map) { - lines.add(indent + key + ":"); - lines.addAll(renderMap((Map) value, depth + 1)); - } else { - lines.add(indent + key + ":"); - lines.addAll(renderSequence((List) value, depth + 1)); - } - }); - return lines; - } - - @SuppressWarnings("unchecked") - private List renderSequence(List sequence, int depth) { - List lines = new ArrayList<>(); - String indent = indent(depth); - for (Object item : sequence) { - if (item instanceof KeyValue) { - KeyValue kv = (KeyValue) item; - addComments(lines, indent, kv); - lines.add(indent + "- " + quoteYamlValue(kv.getValue())); - } else { - // Hang the mapping's first line off the dash, hoisting any comments above it, - // and align the remaining lines with the first - List itemLines = renderMap((Map) item, 0); - int first = 0; - while (itemLines.get(first).startsWith("#")) { - lines.add(indent + itemLines.get(first++)); - } - lines.add(indent + "- " + itemLines.get(first)); - for (int i = first + 1; i < itemLines.size(); i++) { - lines.add(indent + " " + itemLines.get(i)); - } - } - } - return lines; - } - - private void addComments(List lines, String indent, KeyValue kv) { - for (String comment : kv.getComments()) { - lines.add(indent + "#" + comment); - } - } - - private String indent(int depth) { - StringBuilder sb = new StringBuilder(depth * 2); - for (int i = 0; i < depth; i++) { - sb.append(" "); - } - return sb.toString(); - } - - /** - * Groups indexed keys by the base key before their first index (e.g. {@code my.list[0]} and - * {@code my.servers[0].host} group under {@code my.list} / {@code my.servers}), keeping only - * groups that can be faithfully represented as a YAML sequence: indices must be exactly - * {@code 0..n-1}, each index must be either a single scalar or a set of object keys (not both), - * and the base key must not also be used as a plain key. Directly nested indices - * ({@code a[0][1]}) are not converted. - */ - private Map>> groupSequences(List entries) { - Map>> sequences = new LinkedHashMap<>(); - Set invalid = new HashSet<>(); - Set plainKeys = new HashSet<>(); - for (KeyValue entry : entries) { - Matcher indexed = INDEXED_KEY_PATTERN.matcher(entry.getKey()); - if (!indexed.matches()) { - plainKeys.add(entry.getKey()); - continue; - } - String base = indexed.group(1); - int index = Integer.parseInt(indexed.group(2)); - String rest = indexed.group(3); - if (rest.startsWith("[")) { - invalid.add(base); - continue; - } - String itemKey = rest.startsWith(".") ? rest.substring(1) : rest; - sequences.computeIfAbsent(base, k -> new TreeMap<>()) - .computeIfAbsent(index, k -> new ArrayList<>()) - .add(new KeyValue(itemKey, entry.getValue(), entry.getComments())); - } - sequences.entrySet().removeIf(e -> invalid.contains(e.getKey()) || - plainKeys.contains(e.getKey()) || - e.getValue().firstKey() != 0 || - e.getValue().lastKey() != e.getValue().size() - 1 || - e.getValue().values().stream().anyMatch(this::isInvalidSequenceItem)); - return sequences; - } - - /** - * A sequence item must be either exactly one scalar (empty item key) or one or more object keys. - */ - private boolean isInvalidSequenceItem(List item) { - boolean scalar = item.get(0).getKey().isEmpty(); - if (scalar) { - return item.size() > 1; - } - return item.stream().anyMatch(kv -> kv.getKey().isEmpty()); - } - - /** - * Quotes a scalar when leaving it plain would change its meaning: YAML special or control - * characters, leading/trailing whitespace, block indicators, or re-typed values. - */ - private String quoteYamlValue(String value) { - if (value.isEmpty()) { - return "\"\""; - } - boolean needsQuoting = false; - for (char c : value.toCharArray()) { - if (c < ' ' || YAML_SPECIAL_CHARS.indexOf(c) >= 0) { - needsQuoting = true; - break; - } - } - needsQuoting = needsQuoting || - Character.isWhitespace(value.charAt(0)) || - Character.isWhitespace(value.charAt(value.length() - 1)) || - value.startsWith("- ") || "-".equals(value) || - value.startsWith("? ") || "?".equals(value) || - typeChangesWhenPlain(value); - if (!needsQuoting) { - return value; - } - String escaped = value - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\n", "\\n") - .replace("\t", "\\t") - .replace("\r", "\\r") - .replace("\f", "\\f"); - return "\"" + escaped + "\""; - } - - /** - * True when YAML 1.1 resolves the plain scalar to a non-string type whose rendering differs - * from the original text (e.g. {@code on} → {@code true}, {@code 0x1A} → {@code 26}, - * {@code 2001-12-14} → a timestamp), which would change the value Spring binds. - * Values that round-trip textually (e.g. {@code 8080}, {@code true}, {@code 1.5}) stay plain. - */ - private boolean typeChangesWhenPlain(String value) { - if (YAML_RESOLVER.resolve(NodeId.scalar, value, true) == Tag.STR) { - return false; - } - Object loaded = new Yaml(new SafeConstructor(new LoaderOptions())).load(value); - return loaded == null || !value.equals(loaded.toString()); - } } diff --git a/src/main/java/org/openrewrite/java/spring/PropertiesToYamlConverter.java b/src/main/java/org/openrewrite/java/spring/PropertiesToYamlConverter.java new file mode 100644 index 000000000..596604326 --- /dev/null +++ b/src/main/java/org/openrewrite/java/spring/PropertiesToYamlConverter.java @@ -0,0 +1,310 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * 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.java.spring; + +import lombok.Value; +import org.openrewrite.properties.tree.Properties; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.SafeConstructor; +import org.yaml.snakeyaml.nodes.NodeId; +import org.yaml.snakeyaml.nodes.Tag; +import org.yaml.snakeyaml.resolver.Resolver; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.io.UncheckedIOException; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Converts a parsed Spring Boot {@code .properties} file into equivalent YAML text, + * preserving comments and entry order, and quoting scalars so the values Spring binds + * are unchanged. + */ +final class PropertiesToYamlConverter { + + private static final String YAML_SPECIAL_CHARS = ":#[]{}|>&*!'\"%@`"; + private static final Resolver YAML_RESOLVER = new Resolver(); + // Splits a key on its first index: base, index, rest (e.g. my.servers[0].host → my.servers, 0, .host) + private static final Pattern INDEXED_KEY_PATTERN = Pattern.compile("(.+?)\\[(\\d+)](.*)"); + + private PropertiesToYamlConverter() { + } + + /** + * An entry with key and value already unescaped, plus the comment lines that preceded it. + */ + @Value + private static class KeyValue { + String key; + String value; + List comments; + } + + static String convert(Properties.File file) { + List entries = new ArrayList<>(); + List pendingComments = new ArrayList<>(); + for (Properties.Content content : file.getContent()) { + if (content instanceof Properties.Comment) { + pendingComments.add(((Properties.Comment) content).getMessage()); + } else if (content instanceof Properties.Entry) { + entries.add(toKeyValue((Properties.Entry) content, pendingComments)); + pendingComments = new ArrayList<>(); + } + } + List lines = renderMap(buildTree(entries), 0); + for (String comment : pendingComments) { + lines.add("#" + comment); + } + return lines.isEmpty() ? "" : String.join("\n", lines) + "\n"; + } + + /** + * Round-trips the raw entry through {@link java.util.Properties#load(Reader)} so key and + * value are unescaped exactly as Spring would see them at runtime. + */ + private static KeyValue toKeyValue(Properties.Entry entry, List comments) { + java.util.Properties loaded = new java.util.Properties(); + try { + loaded.load(new StringReader(entry.getKey() + "=" + entry.getValue().getText())); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + String key = loaded.stringPropertyNames().iterator().next(); + return new KeyValue(key, loaded.getProperty(key), comments); + } + + /** + * Builds a nested structure of mappings (in first-seen key order), sequences and scalars. + * A key that would nest inside another key's value (e.g. {@code a.b.c=2} alongside + * {@code a.b=1}) keeps its conflicting remainder as a literal dotted key, which Spring's + * relaxed binding reads the same way. + */ + private static Map buildTree(List entries) { + Map>> sequences = groupSequences(entries); + Set terminalKeys = new HashSet<>(); + for (KeyValue entry : entries) { + Matcher indexed = INDEXED_KEY_PATTERN.matcher(entry.getKey()); + terminalKeys.add(indexed.matches() && sequences.containsKey(indexed.group(1)) ? + indexed.group(1) : entry.getKey()); + } + Map root = new LinkedHashMap<>(); + Set insertedSequences = new HashSet<>(); + for (KeyValue entry : entries) { + Matcher indexed = INDEXED_KEY_PATTERN.matcher(entry.getKey()); + if (indexed.matches() && sequences.containsKey(indexed.group(1))) { + if (insertedSequences.add(indexed.group(1))) { + insert(root, indexed.group(1), buildSequence(sequences.get(indexed.group(1))), terminalKeys); + } + continue; + } + insert(root, entry.getKey(), entry, terminalKeys); + } + return root; + } + + /** + * A sequence item is either a scalar (single entry with an empty key) or a nested mapping. + */ + private static List buildSequence(NavigableMap> items) { + List sequence = new ArrayList<>(); + for (List item : items.values()) { + sequence.add(item.get(0).getKey().isEmpty() ? item.get(0) : buildTree(item)); + } + return sequence; + } + + /** + * Never creates a mapping at a path that is itself a terminal key: from that point on the + * remainder stays a literal dotted key, so a scalar (or sequence) and its dotted descendants + * can coexist without duplicate YAML keys, regardless of entry order. + */ + @SuppressWarnings("unchecked") + private static void insert(Map root, String key, Object value, Set terminalKeys) { + Map current = root; + String[] segments = key.split("\\."); + StringBuilder path = new StringBuilder(); + for (int i = 0; i < segments.length - 1; i++) { + path.append(i == 0 ? "" : ".").append(segments[i]); + if (terminalKeys.contains(path.toString())) { + current.put(String.join(".", Arrays.asList(segments).subList(i, segments.length)), value); + return; + } + current = (Map) current.computeIfAbsent(segments[i], k -> new LinkedHashMap<>()); + } + current.put(segments[segments.length - 1], value); + } + + @SuppressWarnings("unchecked") + private static List renderMap(Map map, int depth) { + List lines = new ArrayList<>(); + String indent = indent(depth); + map.forEach((key, value) -> { + if (value instanceof KeyValue) { + KeyValue kv = (KeyValue) value; + addComments(lines, indent, kv); + lines.add(indent + key + ": " + quoteYamlValue(kv.getValue())); + } else if (value instanceof Map) { + lines.add(indent + key + ":"); + lines.addAll(renderMap((Map) value, depth + 1)); + } else { + lines.add(indent + key + ":"); + lines.addAll(renderSequence((List) value, depth + 1)); + } + }); + return lines; + } + + @SuppressWarnings("unchecked") + private static List renderSequence(List sequence, int depth) { + List lines = new ArrayList<>(); + String indent = indent(depth); + for (Object item : sequence) { + if (item instanceof KeyValue) { + KeyValue kv = (KeyValue) item; + addComments(lines, indent, kv); + lines.add(indent + "- " + quoteYamlValue(kv.getValue())); + } else { + // Hang the mapping's first line off the dash, hoisting any comments above it, + // and align the remaining lines with the first + List itemLines = renderMap((Map) item, 0); + int first = 0; + while (itemLines.get(first).startsWith("#")) { + lines.add(indent + itemLines.get(first++)); + } + lines.add(indent + "- " + itemLines.get(first)); + for (int i = first + 1; i < itemLines.size(); i++) { + lines.add(indent + " " + itemLines.get(i)); + } + } + } + return lines; + } + + private static void addComments(List lines, String indent, KeyValue kv) { + for (String comment : kv.getComments()) { + lines.add(indent + "#" + comment); + } + } + + private static String indent(int depth) { + StringBuilder sb = new StringBuilder(depth * 2); + for (int i = 0; i < depth; i++) { + sb.append(" "); + } + return sb.toString(); + } + + /** + * Groups indexed keys by the base key before their first index (e.g. {@code my.list[0]} and + * {@code my.servers[0].host} group under {@code my.list} / {@code my.servers}), keeping only + * groups that can be faithfully represented as a YAML sequence: indices must be exactly + * {@code 0..n-1}, each index must be either a single scalar or a set of object keys (not both), + * and the base key must not also be used as a plain key. Directly nested indices + * ({@code a[0][1]}) are not converted. + */ + private static Map>> groupSequences(List entries) { + Map>> sequences = new LinkedHashMap<>(); + Set invalid = new HashSet<>(); + Set plainKeys = new HashSet<>(); + for (KeyValue entry : entries) { + Matcher indexed = INDEXED_KEY_PATTERN.matcher(entry.getKey()); + if (!indexed.matches()) { + plainKeys.add(entry.getKey()); + continue; + } + String base = indexed.group(1); + int index = Integer.parseInt(indexed.group(2)); + String rest = indexed.group(3); + if (rest.startsWith("[")) { + invalid.add(base); + continue; + } + String itemKey = rest.startsWith(".") ? rest.substring(1) : rest; + sequences.computeIfAbsent(base, k -> new TreeMap<>()) + .computeIfAbsent(index, k -> new ArrayList<>()) + .add(new KeyValue(itemKey, entry.getValue(), entry.getComments())); + } + sequences.entrySet().removeIf(e -> invalid.contains(e.getKey()) || + plainKeys.contains(e.getKey()) || + e.getValue().firstKey() != 0 || + e.getValue().lastKey() != e.getValue().size() - 1 || + e.getValue().values().stream().anyMatch(PropertiesToYamlConverter::isInvalidSequenceItem)); + return sequences; + } + + /** + * A sequence item must be either exactly one scalar (empty item key) or one or more object keys. + */ + private static boolean isInvalidSequenceItem(List item) { + boolean scalar = item.get(0).getKey().isEmpty(); + if (scalar) { + return item.size() > 1; + } + return item.stream().anyMatch(kv -> kv.getKey().isEmpty()); + } + + /** + * Quotes a scalar when leaving it plain would change its meaning: YAML special or control + * characters, leading/trailing whitespace, block indicators, or re-typed values. + */ + private static String quoteYamlValue(String value) { + if (value.isEmpty()) { + return "\"\""; + } + boolean needsQuoting = false; + for (char c : value.toCharArray()) { + if (c < ' ' || YAML_SPECIAL_CHARS.indexOf(c) >= 0) { + needsQuoting = true; + break; + } + } + needsQuoting = needsQuoting || + Character.isWhitespace(value.charAt(0)) || + Character.isWhitespace(value.charAt(value.length() - 1)) || + value.startsWith("- ") || "-".equals(value) || + value.startsWith("? ") || "?".equals(value) || + typeChangesWhenPlain(value); + if (!needsQuoting) { + return value; + } + String escaped = value + .replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\t", "\\t") + .replace("\r", "\\r") + .replace("\f", "\\f"); + return "\"" + escaped + "\""; + } + + /** + * True when YAML 1.1 resolves the plain scalar to a non-string type whose rendering differs + * from the original text (e.g. {@code on} → {@code true}, {@code 0x1A} → {@code 26}, + * {@code 2001-12-14} → a timestamp), which would change the value Spring binds. + * Values that round-trip textually (e.g. {@code 8080}, {@code true}, {@code 1.5}) stay plain. + */ + private static boolean typeChangesWhenPlain(String value) { + if (YAML_RESOLVER.resolve(NodeId.scalar, value, true) == Tag.STR) { + return false; + } + Object loaded = new Yaml(new SafeConstructor(new LoaderOptions())).load(value); + return loaded == null || !value.equals(loaded.toString()); + } +} diff --git a/src/test/java/org/openrewrite/java/spring/ConvertPropertiesToYamlTest.java b/src/test/java/org/openrewrite/java/spring/ConvertPropertiesToYamlTest.java index 7d1e3e89a..75e652a3c 100644 --- a/src/test/java/org/openrewrite/java/spring/ConvertPropertiesToYamlTest.java +++ b/src/test/java/org/openrewrite/java/spring/ConvertPropertiesToYamlTest.java @@ -68,37 +68,6 @@ void singlePropertyIsConvertedToNestedYaml() { ); } - @Test - void multiplePropertiesAreConvertedToNestedYaml() { - rewriteRun( - mavenProject("project", - srcMainResources( - properties( - //language=properties - """ - server.port=8080 - spring.application.name=myapp - """, - null, - spec -> spec.path("application.properties") - ), - yaml( - doesNotExist(), - //language=yaml - """ - server: - port: 8080 - spring: - application: - name: myapp - """, - spec -> spec.path("application.yaml") - ) - ) - ) - ); - } - @Test void emptyPropertiesFileIsDeletedWithoutCreatingYaml() { rewriteRun( @@ -458,566 +427,6 @@ void propertiesFileMarkedAsSpringConfigIsConverted() { ); } - @Test - void valueContainingColonIsQuoted() { - rewriteRun( - mavenProject("project", - srcMainResources( - properties( - "spring.datasource.url=jdbc:h2:mem:db", - null, - spec -> spec.path("application.properties") - ), - yaml( - doesNotExist(), - //language=yaml - """ - spring: - datasource: - url: "jdbc:h2:mem:db" - """, - spec -> spec.path("application.yaml") - ) - ) - ) - ); - } - - @Test - void valueContainingHashIsQuoted() { - rewriteRun( - mavenProject("project", - srcMainResources( - properties( - "app.message=hello # world", - null, - spec -> spec.path("application.properties") - ), - yaml( - doesNotExist(), - //language=yaml - """ - app: - message: "hello # world" - """, - spec -> spec.path("application.yaml") - ) - ) - ) - ); - } - - @Test - void emptyValueMapsToEmptyString() { - rewriteRun( - mavenProject("project", - srcMainResources( - properties( - "server.context-path=", - null, - spec -> spec.path("application.properties") - ), - yaml( - doesNotExist(), - //language=yaml - """ - server: - context-path: "" - """, - spec -> spec.path("application.yaml") - ) - ) - ) - ); - } - - @Test - void yamlBooleanLikeValuesAreQuotedToPreserveStringSemantics() { - rewriteRun( - mavenProject("project", - srcMainResources( - properties( - //language=properties - """ - app.a=yes - app.b=off - app.c=on - app.d=true - """, - null, - spec -> spec.path("application.properties") - ), - yaml( - doesNotExist(), - //language=yaml - """ - app: - a: "yes" - b: "off" - c: "on" - d: true - """, - spec -> spec.path("application.yaml") - ) - ) - ) - ); - } - - @Test - void yamlNullLikeValuesAreQuoted() { - rewriteRun( - mavenProject("project", - srcMainResources( - properties( - //language=properties - """ - app.a=null - app.b=~ - """, - null, - spec -> spec.path("application.properties") - ), - yaml( - doesNotExist(), - //language=yaml - """ - app: - a: "null" - b: "~" - """, - spec -> spec.path("application.yaml") - ) - ) - ) - ); - } - - @Test - void valuesReTypedByYamlAreQuotedOthersStayPlain() { - rewriteRun( - mavenProject("project", - srcMainResources( - properties( - //language=properties - """ - app.a=0x1A - app.b=1_000 - app.c=+1 - app.d=2001-12-14 - app.e=1.50 - app.f=8080 - app.g=1.5 - """, - null, - spec -> spec.path("application.properties") - ), - yaml( - doesNotExist(), - //language=yaml - """ - app: - a: "0x1A" - b: "1_000" - c: "+1" - d: "2001-12-14" - e: "1.50" - f: 8080 - g: 1.5 - """, - spec -> spec.path("application.yaml") - ) - ) - ) - ); - } - - @Test - void octalLikeValueIsQuoted() { - rewriteRun( - mavenProject("project", - srcMainResources( - properties( - "app.file-mask=0755", - null, - spec -> spec.path("application.properties") - ), - yaml( - doesNotExist(), - //language=yaml - """ - app: - file-mask: "0755" - """, - spec -> spec.path("application.yaml") - ) - ) - ) - ); - } - - @Test - void escapedNewlineAndTabAreTranslated() { - rewriteRun( - mavenProject("project", - srcMainResources( - properties( - "app.message=line1\\nline2\\tend", - null, - spec -> spec.path("application.properties") - ), - yaml( - doesNotExist(), - """ - app: - message: "line1\\nline2\\tend" - """, - spec -> spec.path("application.yaml") - ) - ) - ) - ); - } - - @Test - void escapedBackslashAndUnicodeAreTranslated() { - rewriteRun( - mavenProject("project", - srcMainResources( - properties( - "app.path=C:\\\\data\\u0021", - null, - spec -> spec.path("application.properties") - ), - yaml( - doesNotExist(), - """ - app: - path: "C:\\\\data!" - """, - spec -> spec.path("application.yaml") - ) - ) - ) - ); - } - - @Test - void escapedKeysAreUnescaped() { - rewriteRun( - mavenProject("project", - srcMainResources( - properties( - """ - my\\ key=1 - a\\:b=2 - """, - null, - spec -> spec.path("application.properties") - ), - yaml( - doesNotExist(), - """ - my key: 1 - a:b: 2 - """, - spec -> spec.path("application.yaml") - ) - ) - ) - ); - } - - @Test - void keyThatIsAlsoAPrefixOfOtherKeysStaysLiteral() { - rewriteRun( - mavenProject("project", - srcMainResources( - properties( - //language=properties - """ - a=1 - a.b=2 - b.c.d=3 - b.c=4 - """, - null, - spec -> spec.path("application.properties") - ), - yaml( - doesNotExist(), - //language=yaml - """ - a: 1 - a.b: 2 - b: - c.d: 3 - c: 4 - """, - spec -> spec.path("application.yaml") - ) - ) - ) - ); - } - - @Test - void commentLinesInPropertiesArePreserved() { - rewriteRun( - mavenProject("project", - srcMainResources( - properties( - //language=properties - """ - # header comment - server.port=8080 - ! also a comment - spring.application.name=myapp - # trailing comment - """, - null, - spec -> spec.path("application.properties") - ), - yaml( - doesNotExist(), - //language=yaml - """ - server: - # header comment - port: 8080 - spring: - application: - # also a comment - name: myapp - # trailing comment - """, - spec -> spec.path("application.yaml") - ) - ) - ) - ); - } - - @Test - void indexedKeysAreConvertedToYamlSequence() { - rewriteRun( - mavenProject("project", - srcMainResources( - properties( - //language=properties - """ - my.list[0]=a - other.key=x - my.list[1]=b - """, - null, - spec -> spec.path("application.properties") - ), - yaml( - doesNotExist(), - //language=yaml - """ - my: - list: - - a - - b - other: - key: x - """, - spec -> spec.path("application.yaml") - ) - ) - ) - ); - } - - @Test - void outOfOrderIndexedKeysAreSortedIntoSequence() { - rewriteRun( - mavenProject("project", - srcMainResources( - properties( - //language=properties - """ - my.list[1]=b - my.list[0]=a - """, - null, - spec -> spec.path("application.properties") - ), - yaml( - doesNotExist(), - //language=yaml - """ - my: - list: - - a - - b - """, - spec -> spec.path("application.yaml") - ) - ) - ) - ); - } - - @Test - void sequenceValuesAreQuotedWhenNeeded() { - rewriteRun( - mavenProject("project", - srcMainResources( - properties( - //language=properties - """ - app.urls[0]=jdbc:h2:mem:db - app.urls[1]=plain - """, - null, - spec -> spec.path("application.properties") - ), - yaml( - doesNotExist(), - //language=yaml - """ - app: - urls: - - "jdbc:h2:mem:db" - - plain - """, - spec -> spec.path("application.yaml") - ) - ) - ) - ); - } - - @Test - void nonContiguousIndexedKeysRemainFlat() { - rewriteRun( - mavenProject("project", - srcMainResources( - properties( - //language=properties - """ - my.list[0]=a - my.list[2]=c - """, - null, - spec -> spec.path("application.properties") - ), - yaml( - doesNotExist(), - //language=yaml - """ - my: - list[0]: a - list[2]: c - """, - spec -> spec.path("application.yaml") - ) - ) - ) - ); - } - - @Test - void objectListKeysAreConvertedToYamlSequence() { - rewriteRun( - mavenProject("project", - srcMainResources( - properties( - //language=properties - """ - my.servers[0].host=alpha - my.servers[0].port=8080 - my.servers[1].host=beta - my.servers[1].port=9090 - """, - null, - spec -> spec.path("application.properties") - ), - yaml( - doesNotExist(), - //language=yaml - """ - my: - servers: - - host: alpha - port: 8080 - - host: beta - port: 9090 - """, - spec -> spec.path("application.yaml") - ) - ) - ) - ); - } - - @Test - void nestedListInsideObjectListIsConverted() { - rewriteRun( - mavenProject("project", - srcMainResources( - properties( - //language=properties - """ - my.servers[0].host=alpha - my.servers[0].tags[0]=x - my.servers[0].tags[1]=y - """, - null, - spec -> spec.path("application.properties") - ), - yaml( - doesNotExist(), - //language=yaml - """ - my: - servers: - - host: alpha - tags: - - x - - y - """, - spec -> spec.path("application.yaml") - ) - ) - ) - ); - } - - @Test - void nonContiguousObjectListKeysRemainFlat() { - rewriteRun( - mavenProject("project", - srcMainResources( - properties( - //language=properties - """ - my.servers[0].host=alpha - my.servers[2].host=gamma - """, - null, - spec -> spec.path("application.properties") - ), - yaml( - doesNotExist(), - //language=yaml - """ - my: - servers[0]: - host: alpha - servers[2]: - host: gamma - """, - spec -> spec.path("application.yaml") - ) - ) - ) - ); - } - @Test void multiModuleProjectConvertsEachModuleIndependently() { rewriteRun( diff --git a/src/test/java/org/openrewrite/java/spring/PropertiesToYamlConverterTest.java b/src/test/java/org/openrewrite/java/spring/PropertiesToYamlConverterTest.java new file mode 100644 index 000000000..e1e7a9750 --- /dev/null +++ b/src/test/java/org/openrewrite/java/spring/PropertiesToYamlConverterTest.java @@ -0,0 +1,354 @@ +/* + * Copyright 2026 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * 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.java.spring; + +import org.intellij.lang.annotations.Language; +import org.junit.jupiter.api.Test; +import org.openrewrite.properties.PropertiesParser; +import org.openrewrite.properties.tree.Properties; + +import static org.assertj.core.api.Assertions.assertThat; + +class PropertiesToYamlConverterTest { + + private static String convert(@Language("properties") String properties) { + Properties.File file = (Properties.File) PropertiesParser.builder().build() + .parse(properties) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Failed to parse properties")); + return PropertiesToYamlConverter.convert(file); + } + + @Test + void emptyFileYieldsEmptyString() { + assertThat(convert("")).isEmpty(); + } + + @Test + void multiplePropertiesBuildNestedMappings() { + assertThat(convert( + """ + server.port=8080 + spring.application.name=myapp + """)) + .isEqualTo( + """ + server: + port: 8080 + spring: + application: + name: myapp + """); + } + + @Test + void valueContainingColonIsQuoted() { + assertThat(convert("spring.datasource.url=jdbc:h2:mem:db")) + .isEqualTo( + """ + spring: + datasource: + url: "jdbc:h2:mem:db" + """); + } + + @Test + void valueContainingHashIsQuoted() { + assertThat(convert("app.message=hello # world")) + .isEqualTo( + """ + app: + message: "hello # world" + """); + } + + @Test + void emptyValueMapsToEmptyString() { + assertThat(convert("server.context-path=")) + .isEqualTo( + """ + server: + context-path: "" + """); + } + + @Test + void yamlBooleanLikeValuesAreQuotedToPreserveStringSemantics() { + assertThat(convert( + """ + app.a=yes + app.b=off + app.c=on + app.d=true + """)) + .isEqualTo( + """ + app: + a: "yes" + b: "off" + c: "on" + d: true + """); + } + + @Test + void yamlNullLikeValuesAreQuoted() { + assertThat(convert( + """ + app.a=null + app.b=~ + """)) + .isEqualTo( + """ + app: + a: "null" + b: "~" + """); + } + + @Test + void valuesReTypedByYamlAreQuotedOthersStayPlain() { + assertThat(convert( + """ + app.a=0x1A + app.b=1_000 + app.c=+1 + app.d=2001-12-14 + app.e=1.50 + app.f=8080 + app.g=1.5 + """)) + .isEqualTo( + """ + app: + a: "0x1A" + b: "1_000" + c: "+1" + d: "2001-12-14" + e: "1.50" + f: 8080 + g: 1.5 + """); + } + + @Test + void octalLikeValueIsQuoted() { + assertThat(convert("app.file-mask=0755")) + .isEqualTo( + """ + app: + file-mask: "0755" + """); + } + + @Test + void escapedNewlineAndTabAreTranslated() { + assertThat(convert("app.message=line1\\nline2\\tend")) + .isEqualTo( + """ + app: + message: "line1\\nline2\\tend" + """); + } + + @Test + void escapedBackslashAndUnicodeAreTranslated() { + assertThat(convert("app.path=C:\\\\data\\u0021")) + .isEqualTo( + """ + app: + path: "C:\\\\data!" + """); + } + + @Test + void escapedKeysAreUnescaped() { + assertThat(convert( + """ + my\\ key=1 + a\\:b=2 + """)) + .isEqualTo( + """ + my key: 1 + a:b: 2 + """); + } + + @Test + void keyThatIsAlsoAPrefixOfOtherKeysStaysLiteral() { + assertThat(convert( + """ + a=1 + a.b=2 + b.c.d=3 + b.c=4 + """)) + .isEqualTo( + """ + a: 1 + a.b: 2 + b: + c.d: 3 + c: 4 + """); + } + + @Test + void commentLinesArePreserved() { + assertThat(convert( + """ + # header comment + server.port=8080 + ! also a comment + spring.application.name=myapp + # trailing comment + """)) + .isEqualTo( + """ + server: + # header comment + port: 8080 + spring: + application: + # also a comment + name: myapp + # trailing comment + """); + } + + @Test + void indexedKeysAreConvertedToYamlSequence() { + assertThat(convert( + """ + my.list[0]=a + other.key=x + my.list[1]=b + """)) + .isEqualTo( + """ + my: + list: + - a + - b + other: + key: x + """); + } + + @Test + void outOfOrderIndexedKeysAreSortedIntoSequence() { + assertThat(convert( + """ + my.list[1]=b + my.list[0]=a + """)) + .isEqualTo( + """ + my: + list: + - a + - b + """); + } + + @Test + void sequenceValuesAreQuotedWhenNeeded() { + assertThat(convert( + """ + app.urls[0]=jdbc:h2:mem:db + app.urls[1]=plain + """)) + .isEqualTo( + """ + app: + urls: + - "jdbc:h2:mem:db" + - plain + """); + } + + @Test + void nonContiguousIndexedKeysRemainFlat() { + assertThat(convert( + """ + my.list[0]=a + my.list[2]=c + """)) + .isEqualTo( + """ + my: + list[0]: a + list[2]: c + """); + } + + @Test + void objectListKeysAreConvertedToYamlSequence() { + assertThat(convert( + """ + my.servers[0].host=alpha + my.servers[0].port=8080 + my.servers[1].host=beta + my.servers[1].port=9090 + """)) + .isEqualTo( + """ + my: + servers: + - host: alpha + port: 8080 + - host: beta + port: 9090 + """); + } + + @Test + void nestedListInsideObjectListIsConverted() { + assertThat(convert( + """ + my.servers[0].host=alpha + my.servers[0].tags[0]=x + my.servers[0].tags[1]=y + """)) + .isEqualTo( + """ + my: + servers: + - host: alpha + tags: + - x + - y + """); + } + + @Test + void nonContiguousObjectListKeysRemainFlat() { + assertThat(convert( + """ + my.servers[0].host=alpha + my.servers[2].host=gamma + """)) + .isEqualTo( + """ + my: + servers[0]: + host: alpha + servers[2]: + host: gamma + """); + } +} From d6f6c57cd9313b08f0069748fb9983c86da5f2c1 Mon Sep 17 00:00:00 2001 From: Marius Barbulescu Date: Tue, 14 Jul 2026 06:57:52 +0200 Subject: [PATCH 03/13] delete only properties files successfully converted to yaml --- .../java/spring/ConvertPropertiesToYaml.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java b/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java index 5adef576e..31a6ab24d 100644 --- a/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java +++ b/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java @@ -69,6 +69,9 @@ public static class Accumulator { // parent directory -> file name stem (e.g. "application-dev") -> existing .yml/.yaml file final Map> existingYaml = new HashMap<>(); final Set fileNamesReferencedFromJava = new HashSet<>(); + // Paths whose conversion succeeded (YAML generated, or the file had no content + // to carry over); only these may be deleted + final Set converted = new HashSet<>(); } @Override @@ -141,6 +144,7 @@ public Collection generate(Accumulator acc, ExecutionContext ctx) { } String yamlContent = PropertiesToYamlConverter.convert((Properties.File) value); if (yamlContent.isEmpty()) { + acc.converted.add(propertiesPath); return; } YamlParser.builder().build() @@ -151,7 +155,10 @@ public Collection generate(Accumulator acc, ExecutionContext ctx) { // Copy markers (SourceSet, JavaProject, …) from the source file // so the generated file is placed in the correct source set / module .withMarkers(value.getMarkers())) - .ifPresent(newFiles::add); + .ifPresent(newFile -> { + newFiles.add(newFile); + acc.converted.add(propertiesPath); + }); }); return newFiles; } @@ -180,7 +187,6 @@ public TreeVisitor getVisitor(Accumulator acc) { Path conflictingYaml = findExistingYaml(acc, sourcePath); if (conflictingYaml != null) { - // Attach a visible skip message instead of deleting return SearchResult.found( propertiesFile, "Skipped: a corresponding YAML file already exists at '" + conflictingYaml + "'. " + @@ -189,8 +195,7 @@ public TreeVisitor getVisitor(Accumulator acc) { "could change the effective configuration." ); } - // Delete the original .properties file - return null; + return acc.converted.contains(sourcePath) ? null : tree; } }; } From 1ca7f9ac2ce5cb718a9ba97bc729ff1e74f8d284 Mon Sep 17 00:00:00 2001 From: Marius Barbulescu Date: Tue, 14 Jul 2026 07:12:26 +0200 Subject: [PATCH 04/13] unify properties to yaml skip logic --- .../java/spring/ConvertPropertiesToYaml.java | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java b/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java index 31a6ab24d..05d88c7aa 100644 --- a/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java +++ b/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java @@ -138,8 +138,7 @@ public Collection generate(Accumulator acc, ExecutionContext ctx) { } List newFiles = new ArrayList<>(); acc.toConvert.forEach((propertiesPath, value) -> { - if (findExistingYaml(acc, propertiesPath) != null || - acc.fileNamesReferencedFromJava.contains(propertiesPath.getFileName().toString())) { + if (skipReason(acc, propertiesPath) != null) { return; } String yamlContent = PropertiesToYamlConverter.convert((Properties.File) value); @@ -177,29 +176,31 @@ public TreeVisitor getVisitor(Accumulator acc) { return tree; } - if (acc.fileNamesReferencedFromJava.contains(sourcePath.getFileName().toString())) { - return SearchResult.found( - propertiesFile, - "Skipped: this file is referenced from Java sources (e.g. `@PropertySource`), " + - "which cannot load YAML files. Update those references before converting." - ); - } - - Path conflictingYaml = findExistingYaml(acc, sourcePath); - if (conflictingYaml != null) { - return SearchResult.found( - propertiesFile, - "Skipped: a corresponding YAML file already exists at '" + conflictingYaml + "'. " + - "Merge these properties into it manually; when both files exist the " + - "`.properties` values take precedence, so converting automatically " + - "could change the effective configuration." - ); + String skipReason = skipReason(acc, sourcePath); + if (skipReason != null) { + // Attach a visible skip message instead of deleting + return SearchResult.found(propertiesFile, skipReason); } return acc.converted.contains(sourcePath) ? null : tree; } }; } + private @Nullable String skipReason(Accumulator acc, Path propertiesPath) { + if (acc.fileNamesReferencedFromJava.contains(propertiesPath.getFileName().toString())) { + return "Skipped: this file is referenced from Java sources (e.g. `@PropertySource`), " + + "which cannot load YAML files. Update those references before converting."; + } + Path conflictingYaml = findExistingYaml(acc, propertiesPath); + if (conflictingYaml != null) { + return "Skipped: a corresponding YAML file already exists at '" + conflictingYaml + "'. " + + "Merge these properties into it manually; when both files exist the " + + "`.properties` values take precedence, so converting automatically " + + "could change the effective configuration."; + } + return null; + } + private @Nullable Path findExistingYaml(Accumulator acc, Path propertiesPath) { Path parent = propertiesPath.getParent() != null ? propertiesPath.getParent() : Paths.get(""); String stem = propertiesPath.getFileName().toString().replaceAll("\\.properties$", ""); From b2b1578be6b068661b28ac5623c11753c53ca88d Mon Sep 17 00:00:00 2001 From: Marius Barbulescu Date: Tue, 14 Jul 2026 07:24:36 +0200 Subject: [PATCH 05/13] accumulator keeps converted yaml contents and no more LSTs --- .../java/spring/ConvertPropertiesToYaml.java | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java b/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java index 05d88c7aa..2bf7ce6b4 100644 --- a/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java +++ b/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java @@ -22,6 +22,7 @@ import org.openrewrite.java.JavaIsoVisitor; import org.openrewrite.java.tree.J; import org.openrewrite.java.tree.JavaSourceFile; +import org.openrewrite.marker.Markers; import org.openrewrite.marker.SearchResult; import org.openrewrite.properties.tree.Properties; import org.openrewrite.yaml.YamlParser; @@ -64,8 +65,14 @@ public String getDescription() { "when a corresponding `.yml` or `.yaml` file already exists."; } + @Value + static class PendingConversion { + String yamlContent; + Markers markers; + } + public static class Accumulator { - final Map toConvert = new LinkedHashMap<>(); + final Map toConvert = new LinkedHashMap<>(); // parent directory -> file name stem (e.g. "application-dev") -> existing .yml/.yaml file final Map> existingYaml = new HashMap<>(); final Set fileNamesReferencedFromJava = new HashSet<>(); @@ -105,7 +112,9 @@ public TreeVisitor getScanner(Accumulator acc) { if (tree instanceof Properties.File && PROPERTIES_NAME_PATTERN.matcher(fileName).matches() && isSpringConfigFile.visit(tree, ctx) != tree) { - acc.toConvert.put(sourcePath, source); + acc.toConvert.put(sourcePath, new PendingConversion( + PropertiesToYamlConverter.convert((Properties.File) tree), + source.getMarkers())); } // Track file names referenced from Java string literals (e.g. @PropertySource, @@ -137,23 +146,20 @@ public Collection generate(Accumulator acc, ExecutionContext ctx) { return emptyList(); } List newFiles = new ArrayList<>(); - acc.toConvert.forEach((propertiesPath, value) -> { + acc.toConvert.forEach((propertiesPath, pending) -> { if (skipReason(acc, propertiesPath) != null) { return; } - String yamlContent = PropertiesToYamlConverter.convert((Properties.File) value); - if (yamlContent.isEmpty()) { + if (pending.getYamlContent().isEmpty()) { acc.converted.add(propertiesPath); return; } YamlParser.builder().build() - .parse(yamlContent) + .parse(pending.getYamlContent()) .findFirst() .map(brandNew -> (SourceFile) brandNew .withSourcePath(toYamlPath(propertiesPath)) - // Copy markers (SourceSet, JavaProject, …) from the source file - // so the generated file is placed in the correct source set / module - .withMarkers(value.getMarkers())) + .withMarkers(pending.getMarkers())) .ifPresent(newFile -> { newFiles.add(newFile); acc.converted.add(propertiesPath); From 62b1380e6ca4a3d2f0e522f49c03b4be88f3d71d Mon Sep 17 00:00:00 2001 From: Marius Barbulescu Date: Tue, 14 Jul 2026 07:32:20 +0200 Subject: [PATCH 06/13] simplify scanner's visit method --- .../java/spring/ConvertPropertiesToYaml.java | 89 ++++++++++--------- 1 file changed, 47 insertions(+), 42 deletions(-) diff --git a/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java b/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java index 2bf7ce6b4..fd79e4bcf 100644 --- a/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java +++ b/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java @@ -88,56 +88,61 @@ public Accumulator getInitialValue(ExecutionContext ctx) { @Override public TreeVisitor getScanner(Accumulator acc) { - TreeVisitor isSpringConfigFile = new IsPossibleSpringConfigFile(); return new TreeVisitor() { @Override public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) { - if (!(tree instanceof SourceFile)) { - return tree; - } - SourceFile source = (SourceFile) tree; - Path sourcePath = source.getSourcePath(); - String fileName = sourcePath.getFileName() == null ? "" : sourcePath.getFileName().toString(); - - // Track existing YAML files by path, regardless of how they were parsed - // (a sibling .yml that failed to parse as YAML must still prevent conversion) - if (YAML_NAME_PATTERN.matcher(fileName).matches()) { - String stem = fileName.replaceAll("\\.(yml|yaml)$", ""); - Path parent = sourcePath.getParent() != null ? sourcePath.getParent() : Paths.get(""); - acc.existingYaml.computeIfAbsent(parent, k -> new HashMap<>()).putIfAbsent(stem, sourcePath); - return tree; + if (tree instanceof SourceFile) { + SourceFile source = (SourceFile) tree; + trackExistingYaml(acc, source); + trackCandidate(acc, source, ctx); + collectJavaReferences(acc, source); } + return tree; + } + }; + } - // Track properties files that are candidates for conversion - if (tree instanceof Properties.File && - PROPERTIES_NAME_PATTERN.matcher(fileName).matches() && - isSpringConfigFile.visit(tree, ctx) != tree) { - acc.toConvert.put(sourcePath, new PendingConversion( - PropertiesToYamlConverter.convert((Properties.File) tree), - source.getMarkers())); - } + private static void trackExistingYaml(Accumulator acc, SourceFile source) { + String fileName = fileName(source); + if (YAML_NAME_PATTERN.matcher(fileName).matches()) { + Path sourcePath = source.getSourcePath(); + String stem = fileName.replaceAll("\\.(yml|yaml)$", ""); + Path parent = sourcePath.getParent() != null ? sourcePath.getParent() : Paths.get(""); + acc.existingYaml.computeIfAbsent(parent, k -> new HashMap<>()).putIfAbsent(stem, sourcePath); + } + } - // Track file names referenced from Java string literals (e.g. @PropertySource, - // @TestPropertySource, resource loading): such references cannot load YAML, - // so the referenced files must not be converted - if (tree instanceof JavaSourceFile) { - new JavaIsoVisitor() { - @Override - public J.Literal visitLiteral(J.Literal literal, ExecutionContext ctx) { - if (literal.getValue() instanceof String) { - Matcher reference = REFERENCED_FILE_NAME_PATTERN.matcher((String) literal.getValue()); - while (reference.find()) { - acc.fileNamesReferencedFromJava.add(reference.group()); - } - } - return literal; - } - }.visit(tree, ctx); - } + private static void trackCandidate(Accumulator acc, SourceFile source, ExecutionContext ctx) { + if (source instanceof Properties.File && + PROPERTIES_NAME_PATTERN.matcher(fileName(source)).matches() && + new IsPossibleSpringConfigFile().visit(source, ctx) != source) { + acc.toConvert.put(source.getSourcePath(), new PendingConversion( + PropertiesToYamlConverter.convert((Properties.File) source), + source.getMarkers())); + } + } - return tree; + private static void collectJavaReferences(Accumulator acc, SourceFile source) { + if (!(source instanceof JavaSourceFile)) { + return; + } + new JavaIsoVisitor>() { + @Override + public J.Literal visitLiteral(J.Literal literal, Set referencedFileNames) { + if (literal.getValue() instanceof String) { + Matcher reference = REFERENCED_FILE_NAME_PATTERN.matcher((String) literal.getValue()); + while (reference.find()) { + referencedFileNames.add(reference.group()); + } + } + return literal; } - }; + }.visit(source, acc.fileNamesReferencedFromJava); + } + + private static String fileName(SourceFile source) { + Path fileName = source.getSourcePath().getFileName(); + return fileName == null ? "" : fileName.toString(); } @Override From 9c2ebe66c8ae4766a3dd8139eb9aacfa683ad4fe Mon Sep 17 00:00:00 2001 From: Marius Barbulescu Date: Tue, 14 Jul 2026 07:57:34 +0200 Subject: [PATCH 07/13] make PropertiesToYamlConverter tree typed --- .../spring/PropertiesToYamlConverter.java | 93 ++++++++++++------- 1 file changed, 59 insertions(+), 34 deletions(-) diff --git a/src/main/java/org/openrewrite/java/spring/PropertiesToYamlConverter.java b/src/main/java/org/openrewrite/java/spring/PropertiesToYamlConverter.java index 596604326..4985a33a9 100644 --- a/src/main/java/org/openrewrite/java/spring/PropertiesToYamlConverter.java +++ b/src/main/java/org/openrewrite/java/spring/PropertiesToYamlConverter.java @@ -57,6 +57,32 @@ private static class KeyValue { List comments; } + /** + * A node in the YAML tree built from the entries: a {@link Scalar} value, + * a {@link Mapping} of keys to child nodes, or a {@link Sequence} of items. + */ + private interface Node { + } + + @Value + private static class Scalar implements Node { + String value; + List comments; + } + + private static class Mapping implements Node { + final Map entries = new LinkedHashMap<>(); + + Mapping childMapping(String key) { + return (Mapping) entries.computeIfAbsent(key, k -> new Mapping()); + } + } + + @Value + private static class Sequence implements Node { + List items; + } + static String convert(Properties.File file) { List entries = new ArrayList<>(); List pendingComments = new ArrayList<>(); @@ -68,7 +94,7 @@ static String convert(Properties.File file) { pendingComments = new ArrayList<>(); } } - List lines = renderMap(buildTree(entries), 0); + List lines = renderMapping(buildTree(entries), 0); for (String comment : pendingComments) { lines.add("#" + comment); } @@ -96,7 +122,7 @@ private static KeyValue toKeyValue(Properties.Entry entry, List comments * {@code a.b=1}) keeps its conflicting remainder as a literal dotted key, which Spring's * relaxed binding reads the same way. */ - private static Map buildTree(List entries) { + private static Mapping buildTree(List entries) { Map>> sequences = groupSequences(entries); Set terminalKeys = new HashSet<>(); for (KeyValue entry : entries) { @@ -104,7 +130,7 @@ private static Map buildTree(List entries) { terminalKeys.add(indexed.matches() && sequences.containsKey(indexed.group(1)) ? indexed.group(1) : entry.getKey()); } - Map root = new LinkedHashMap<>(); + Mapping root = new Mapping(); Set insertedSequences = new HashSet<>(); for (KeyValue entry : entries) { Matcher indexed = INDEXED_KEY_PATTERN.matcher(entry.getKey()); @@ -114,7 +140,7 @@ private static Map buildTree(List entries) { } continue; } - insert(root, entry.getKey(), entry, terminalKeys); + insert(root, entry.getKey(), new Scalar(entry.getValue(), entry.getComments()), terminalKeys); } return root; } @@ -122,12 +148,14 @@ private static Map buildTree(List entries) { /** * A sequence item is either a scalar (single entry with an empty key) or a nested mapping. */ - private static List buildSequence(NavigableMap> items) { - List sequence = new ArrayList<>(); + private static Sequence buildSequence(NavigableMap> items) { + List sequence = new ArrayList<>(); for (List item : items.values()) { - sequence.add(item.get(0).getKey().isEmpty() ? item.get(0) : buildTree(item)); + sequence.add(item.get(0).getKey().isEmpty() ? + new Scalar(item.get(0).getValue(), item.get(0).getComments()) : + buildTree(item)); } - return sequence; + return new Sequence(sequence); } /** @@ -135,55 +163,52 @@ private static List buildSequence(NavigableMap> * remainder stays a literal dotted key, so a scalar (or sequence) and its dotted descendants * can coexist without duplicate YAML keys, regardless of entry order. */ - @SuppressWarnings("unchecked") - private static void insert(Map root, String key, Object value, Set terminalKeys) { - Map current = root; + private static void insert(Mapping root, String key, Node value, Set terminalKeys) { + Mapping current = root; String[] segments = key.split("\\."); StringBuilder path = new StringBuilder(); for (int i = 0; i < segments.length - 1; i++) { path.append(i == 0 ? "" : ".").append(segments[i]); if (terminalKeys.contains(path.toString())) { - current.put(String.join(".", Arrays.asList(segments).subList(i, segments.length)), value); + current.entries.put(String.join(".", Arrays.asList(segments).subList(i, segments.length)), value); return; } - current = (Map) current.computeIfAbsent(segments[i], k -> new LinkedHashMap<>()); + current = current.childMapping(segments[i]); } - current.put(segments[segments.length - 1], value); + current.entries.put(segments[segments.length - 1], value); } - @SuppressWarnings("unchecked") - private static List renderMap(Map map, int depth) { + private static List renderMapping(Mapping mapping, int depth) { List lines = new ArrayList<>(); String indent = indent(depth); - map.forEach((key, value) -> { - if (value instanceof KeyValue) { - KeyValue kv = (KeyValue) value; - addComments(lines, indent, kv); - lines.add(indent + key + ": " + quoteYamlValue(kv.getValue())); - } else if (value instanceof Map) { + mapping.entries.forEach((key, node) -> { + if (node instanceof Scalar) { + Scalar scalar = (Scalar) node; + addComments(lines, indent, scalar.getComments()); + lines.add(indent + key + ": " + quoteYamlValue(scalar.getValue())); + } else if (node instanceof Mapping) { lines.add(indent + key + ":"); - lines.addAll(renderMap((Map) value, depth + 1)); + lines.addAll(renderMapping((Mapping) node, depth + 1)); } else { lines.add(indent + key + ":"); - lines.addAll(renderSequence((List) value, depth + 1)); + lines.addAll(renderSequence((Sequence) node, depth + 1)); } }); return lines; } - @SuppressWarnings("unchecked") - private static List renderSequence(List sequence, int depth) { + private static List renderSequence(Sequence sequence, int depth) { List lines = new ArrayList<>(); String indent = indent(depth); - for (Object item : sequence) { - if (item instanceof KeyValue) { - KeyValue kv = (KeyValue) item; - addComments(lines, indent, kv); - lines.add(indent + "- " + quoteYamlValue(kv.getValue())); + for (Node item : sequence.getItems()) { + if (item instanceof Scalar) { + Scalar scalar = (Scalar) item; + addComments(lines, indent, scalar.getComments()); + lines.add(indent + "- " + quoteYamlValue(scalar.getValue())); } else { // Hang the mapping's first line off the dash, hoisting any comments above it, // and align the remaining lines with the first - List itemLines = renderMap((Map) item, 0); + List itemLines = renderMapping((Mapping) item, 0); int first = 0; while (itemLines.get(first).startsWith("#")) { lines.add(indent + itemLines.get(first++)); @@ -197,8 +222,8 @@ private static List renderSequence(List sequence, int depth) { return lines; } - private static void addComments(List lines, String indent, KeyValue kv) { - for (String comment : kv.getComments()) { + private static void addComments(List lines, String indent, List comments) { + for (String comment : comments) { lines.add(indent + "#" + comment); } } From 4279717b04a717ba36035da36333b1aba1c02d4c Mon Sep 17 00:00:00 2001 From: Marius Barbulescu Date: Wed, 15 Jul 2026 06:44:37 +0200 Subject: [PATCH 08/13] replace custom escaping with snakeyaml solution --- .../spring/PropertiesToYamlConverter.java | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/openrewrite/java/spring/PropertiesToYamlConverter.java b/src/main/java/org/openrewrite/java/spring/PropertiesToYamlConverter.java index 4985a33a9..6dde9007c 100644 --- a/src/main/java/org/openrewrite/java/spring/PropertiesToYamlConverter.java +++ b/src/main/java/org/openrewrite/java/spring/PropertiesToYamlConverter.java @@ -17,21 +17,26 @@ import lombok.Value; import org.openrewrite.properties.tree.Properties; +import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.SafeConstructor; import org.yaml.snakeyaml.nodes.NodeId; +import org.yaml.snakeyaml.nodes.ScalarNode; import org.yaml.snakeyaml.nodes.Tag; import org.yaml.snakeyaml.resolver.Resolver; import java.io.IOException; import java.io.Reader; import java.io.StringReader; +import java.io.StringWriter; import java.io.UncheckedIOException; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; +import static org.yaml.snakeyaml.DumperOptions.ScalarStyle.DOUBLE_QUOTED; + /** * Converts a parsed Spring Boot {@code .properties} file into equivalent YAML text, * preserving comments and entry order, and quoting scalars so the values Spring binds @@ -306,17 +311,18 @@ private static String quoteYamlValue(String value) { value.startsWith("- ") || "-".equals(value) || value.startsWith("? ") || "?".equals(value) || typeChangesWhenPlain(value); - if (!needsQuoting) { - return value; - } - String escaped = value - .replace("\\", "\\\\") - .replace("\"", "\\\"") - .replace("\n", "\\n") - .replace("\t", "\\t") - .replace("\r", "\\r") - .replace("\f", "\\f"); - return "\"" + escaped + "\""; + return needsQuoting ? dumpDoubleQuoted(value) : value; + } + + private static String dumpDoubleQuoted(String value) { + DumperOptions options = new DumperOptions(); + options.setSplitLines(false); + options.setAllowUnicode(true); + + StringWriter out = new StringWriter(); + ScalarNode node = new ScalarNode(Tag.STR, value, null, null, DOUBLE_QUOTED); + new Yaml(options).serialize(node, out); + return out.toString().trim(); } /** From 61c98b56a331fb5fe45157086f5036752fc0db91 Mon Sep 17 00:00:00 2001 From: Marius Barbulescu Date: Wed, 5 Aug 2026 19:54:26 +0200 Subject: [PATCH 09/13] test runtime snakeyaml runtime version matches pinned version --- .../java/spring/PropertiesToYamlConverterTest.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/test/java/org/openrewrite/java/spring/PropertiesToYamlConverterTest.java b/src/test/java/org/openrewrite/java/spring/PropertiesToYamlConverterTest.java index e1e7a9750..e75d0cf72 100644 --- a/src/test/java/org/openrewrite/java/spring/PropertiesToYamlConverterTest.java +++ b/src/test/java/org/openrewrite/java/spring/PropertiesToYamlConverterTest.java @@ -32,6 +32,16 @@ private static String convert(@Language("properties") String properties) { return PropertiesToYamlConverter.convert(file); } + @Test + void runtimeSnakeYamlVersionMatchesCompileTimePin() throws ClassNotFoundException { + Class yamlClass = Class.forName("org.yaml.snakeyaml.Yaml"); + String jarPath = yamlClass.getProtectionDomain().getCodeSource().getLocation().getPath(); + assertThat(jarPath) + .as("SnakeYAML runtime version has drifted from the version pinned by " + + "compileOnly(\"org.yaml:snakeyaml:...\") in build.gradle.kts; update the pin to match") + .contains("snakeyaml-2.6"); + } + @Test void emptyFileYieldsEmptyString() { assertThat(convert("")).isEmpty(); From e01aefee6c8a38cc6eaf005cfef0b07d23e66691 Mon Sep 17 00:00:00 2001 From: Marius Barbulescu Date: Wed, 5 Aug 2026 20:12:26 +0200 Subject: [PATCH 10/13] fix a cross-module false-positive skip bug in ConvertPropertiesToYaml --- .../java/spring/ConvertPropertiesToYaml.java | 16 ++++++-- .../spring/ConvertPropertiesToYamlTest.java | 41 +++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java b/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java index fd79e4bcf..f899291bf 100644 --- a/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java +++ b/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java @@ -20,6 +20,7 @@ import org.jspecify.annotations.Nullable; import org.openrewrite.*; import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.marker.JavaProject; import org.openrewrite.java.tree.J; import org.openrewrite.java.tree.JavaSourceFile; import org.openrewrite.marker.Markers; @@ -75,7 +76,7 @@ public static class Accumulator { final Map toConvert = new LinkedHashMap<>(); // parent directory -> file name stem (e.g. "application-dev") -> existing .yml/.yaml file final Map> existingYaml = new HashMap<>(); - final Set fileNamesReferencedFromJava = new HashSet<>(); + final Map<@Nullable JavaProject, Set> fileNamesReferencedFromJava = new HashMap<>(); // Paths whose conversion succeeded (YAML generated, or the file had no content // to carry over); only these may be deleted final Set converted = new HashSet<>(); @@ -126,6 +127,8 @@ private static void collectJavaReferences(Accumulator acc, SourceFile source) { if (!(source instanceof JavaSourceFile)) { return; } + JavaProject javaProject = source.getMarkers().findFirst(JavaProject.class).orElse(null); + Set referencedFileNames = acc.fileNamesReferencedFromJava.computeIfAbsent(javaProject, k -> new HashSet<>()); new JavaIsoVisitor>() { @Override public J.Literal visitLiteral(J.Literal literal, Set referencedFileNames) { @@ -137,7 +140,7 @@ public J.Literal visitLiteral(J.Literal literal, Set referencedFileNames } return literal; } - }.visit(source, acc.fileNamesReferencedFromJava); + }.visit(source, referencedFileNames); } private static String fileName(SourceFile source) { @@ -198,7 +201,9 @@ public TreeVisitor getVisitor(Accumulator acc) { } private @Nullable String skipReason(Accumulator acc, Path propertiesPath) { - if (acc.fileNamesReferencedFromJava.contains(propertiesPath.getFileName().toString())) { + String fileName = propertiesPath.getFileName().toString(); + JavaProject javaProject = acc.toConvert.get(propertiesPath).getMarkers().findFirst(JavaProject.class).orElse(null); + if (referencedFromJava(acc, null, fileName) || (javaProject != null && referencedFromJava(acc, javaProject, fileName))) { return "Skipped: this file is referenced from Java sources (e.g. `@PropertySource`), " + "which cannot load YAML files. Update those references before converting."; } @@ -212,6 +217,11 @@ public TreeVisitor getVisitor(Accumulator acc) { return null; } + private static boolean referencedFromJava(Accumulator acc, @Nullable JavaProject javaProject, String fileName) { + Set referencedFileNames = acc.fileNamesReferencedFromJava.get(javaProject); + return referencedFileNames != null && referencedFileNames.contains(fileName); + } + private @Nullable Path findExistingYaml(Accumulator acc, Path propertiesPath) { Path parent = propertiesPath.getParent() != null ? propertiesPath.getParent() : Paths.get(""); String stem = propertiesPath.getFileName().toString().replaceAll("\\.properties$", ""); diff --git a/src/test/java/org/openrewrite/java/spring/ConvertPropertiesToYamlTest.java b/src/test/java/org/openrewrite/java/spring/ConvertPropertiesToYamlTest.java index 75e652a3c..c99bf96c6 100644 --- a/src/test/java/org/openrewrite/java/spring/ConvertPropertiesToYamlTest.java +++ b/src/test/java/org/openrewrite/java/spring/ConvertPropertiesToYamlTest.java @@ -470,4 +470,45 @@ void multiModuleProjectConvertsEachModuleIndependently() { ) ); } + + @Test + void javaReferenceInOneModuleDoesNotBlockSameNamedFileInUnrelatedModule() { + // fileNamesReferencedFromJava is scoped per-module (by JavaProject marker), so a + // reference in one module must not block conversion of a same-named file in an + // unrelated module. + rewriteRun( + mavenProject("parent", + mavenProject("service", + srcMainJava( + //language=java + java( + """ + class Config { + String location = "classpath:application-dev.properties"; + } + """ + ) + ) + ), + mavenProject("client", + srcMainResources( + properties( + "server.port=8082", + null, + spec -> spec.path("application-dev.properties") + ), + yaml( + doesNotExist(), + //language=yaml + """ + server: + port: 8082 + """, + spec -> spec.path("application-dev.yaml") + ) + ) + ) + ) + ); + } } From 69f6f5a3107e3d990d2e0b009cd309ac20a1fb57 Mon Sep 17 00:00:00 2001 From: Marius Barbulescu Date: Wed, 5 Aug 2026 20:15:19 +0200 Subject: [PATCH 11/13] add test for duplicate properties key --- .../java/spring/PropertiesToYamlConverterTest.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/test/java/org/openrewrite/java/spring/PropertiesToYamlConverterTest.java b/src/test/java/org/openrewrite/java/spring/PropertiesToYamlConverterTest.java index e75d0cf72..d87991447 100644 --- a/src/test/java/org/openrewrite/java/spring/PropertiesToYamlConverterTest.java +++ b/src/test/java/org/openrewrite/java/spring/PropertiesToYamlConverterTest.java @@ -64,6 +64,20 @@ void multiplePropertiesBuildNestedMappings() { """); } + @Test + void duplicateKeyLastValueWins() { + assertThat(convert( + """ + server.port=8080 + server.port=9090 + """)) + .isEqualTo( + """ + server: + port: 9090 + """); + } + @Test void valueContainingColonIsQuoted() { assertThat(convert("spring.datasource.url=jdbc:h2:mem:db")) From ac28efcba7f1ea9038e9841078af7848be85efb6 Mon Sep 17 00:00:00 2001 From: Marius Barbulescu Date: Wed, 5 Aug 2026 20:18:21 +0200 Subject: [PATCH 12/13] regenerate recipes.csv --- src/main/resources/META-INF/rewrite/recipes.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/META-INF/rewrite/recipes.csv b/src/main/resources/META-INF/rewrite/recipes.csv index b3519209a..26182e41c 100644 --- a/src/main/resources/META-INF/rewrite/recipes.csv +++ b/src/main/resources/META-INF/rewrite/recipes.csv @@ -11,8 +11,8 @@ maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.ChangeMe maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.ChangeSpringPropertyKey,Change the key of a Spring application property,"Change Spring application property keys existing in either Properties or YAML files, and in `@Value`, `@ConditionalOnProperty` or `@SpringBootTest` annotations.",1,,,,,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,"[{""name"":""oldPropertyKey"",""type"":""String"",""displayName"":""Old property key"",""description"":""The property key to rename."",""example"":""management.metrics.binders.*.enabled"",""required"":true},{""name"":""newPropertyKey"",""type"":""String"",""displayName"":""New property key"",""description"":""The new name for the property key."",""example"":""management.metrics.enable.process.files"",""required"":true},{""name"":""except"",""type"":""List"",""displayName"":""Except"",""description"":""Regex. If any of these property keys exist as direct children of `oldPropertyKey`, then they will not be moved to `newPropertyKey`."",""example"":""jvm""}]", maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.ChangeSpringPropertyValue,Change the value of a spring application property,"Change Spring application property values existing in either Properties or YAML files, and in `@Value`, `@ConditionalOnProperty`, `@SpringBootTest`, or `@TestPropertySource` annotations.",1,,,,,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,"[{""name"":""propertyKey"",""type"":""String"",""displayName"":""Property key"",""description"":""The name of the property key whose value is to be changed."",""example"":""management.metrics.binders.files.enabled"",""required"":true},{""name"":""newValue"",""type"":""String"",""displayName"":""New value"",""description"":""The new value to be used for key specified by `propertyKey`."",""example"":""management.metrics.enable.process.files"",""required"":true},{""name"":""oldValue"",""type"":""String"",""displayName"":""Old value"",""description"":""Only change the property value if it matches the configured `oldValue`."",""example"":""false""},{""name"":""regex"",""type"":""Boolean"",""displayName"":""Regex"",""description"":""Default false. If enabled, `oldValue` will be interpreted as a Regular Expression, and capture group contents will be available in `newValue`""},{""name"":""relaxedBinding"",""type"":""Boolean"",""displayName"":""Use relaxed binding"",""description"":""Whether to match the `propertyKey` using [relaxed binding](https://docs.spring.io/spring-boot/docs/2.5.6/reference/html/features.html#features.external-config.typesafe-configuration-properties.relaxed-binding) rules. Default is `true`. Set to `false` to use exact matching.""}]", maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.CommentOutSpringPropertyKey,Comment out Spring properties,"Add comment to specified Spring properties, and optionally comment out the property.",1,,,,,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,"[{""name"":""propertyKey"",""type"":""String"",""displayName"":""Property key"",""description"":""The name of the property key to comment out."",""example"":""management.metrics.binders.files.enabled"",""required"":true},{""name"":""comment"",""type"":""String"",""displayName"":""Comment"",""description"":""Comment to replace the property key."",""example"":""This property is deprecated and no longer applicable starting from Spring Boot 3.0.x"",""required"":true},{""name"":""commentOutProperty"",""type"":""Boolean"",""displayName"":""Comment out property"",""description"":""If `false` the property is kept and only the comment is added. Defaults to `true`.""}]", -maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.ConvertPropertiesToYaml,Convert Spring `application-*.properties` to `application-*.yaml`,Converts Spring Boot `application-*.properties` files to `application-*.yaml`. The original `.properties` file is deleted and its comments are carried over. Conversion is skipped (with a message) when a corresponding `.yml` or `.yaml` file already exists.,1,,,,,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,"[{""name"":""fileExtension"",""type"":""String"",""displayName"":""File extension"",""description"":""The extension to use for the generated YAML files. Defaults to `yaml`."",""example"":""yml"",""valid"":[""yaml"",""yml""]}]", maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.ConvertAutoConfigurationExcludeToExcludeName,Convert auto-configuration `exclude` to `excludeName`,"Rewrite a class literal in the `exclude` attribute of `@SpringBootApplication` or `@EnableAutoConfiguration` to a string literal in the `excludeName` attribute. Useful when the excluded auto-configuration is not on the compile classpath (for example because it became package-private in a newer version of its library). If the target was the last entry in `exclude`, that attribute is removed. If `excludeName` already contains the value, no duplicate is added.",1,,,,,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,"[{""name"":""fullyQualifiedName"",""type"":""String"",""displayName"":""Fully qualified name"",""description"":""The fully qualified name of the auto-configuration class to move from the `exclude` attribute (as a class literal) to the `excludeName` attribute (as a string literal)."",""example"":""org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration"",""required"":true}]", +maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.ConvertPropertiesToYaml,Convert Spring `application-*.properties` to `application-*.yaml`,Converts Spring Boot `application-*.properties` files to `application-*.yaml`. The original `.properties` file is deleted and its comments are carried over. Conversion is skipped (with a message) when a corresponding `.yml` or `.yaml` file already exists.,1,,,,,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,"[{""name"":""fileExtension"",""type"":""String"",""displayName"":""File extension"",""description"":""The extension to use for the generated YAML files. Defaults to `yaml`."",""example"":""yml"",""valid"":[""yaml"",""yml""]}]", maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.DeleteSpringProperty,Delete a spring configuration property,Delete a spring configuration property from any configuration file that contains a matching key.,1,,,,,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,"[{""name"":""propertyKey"",""type"":""String"",""displayName"":""Property key"",""description"":""The property key to delete. Supports glob expressions"",""example"":""management.endpoint.configprops.*"",""required"":true}]", maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.ExpandProperties,Expand Spring YAML properties,Expand YAML properties to not use the dot syntax shortcut.,1,,,,,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,"[{""name"":""sourceFileMask"",""type"":""String"",""displayName"":""Source file mask"",""description"":""An optional source file path mask use to restrict which YAML files will be expanded by this recipe."",""example"":""**/application*.yml""}]", maven,org.openrewrite.recipe:rewrite-spring,org.openrewrite.java.spring.ImplicitWebAnnotationNames,Remove implicit web annotation names,Removes implicit web annotation names.,1,,,,,Spring,Java,,,,,Recipes for upgrading and patching [Spring](https://spring.io/) applications.,Basic building blocks for transforming Java code.,, From b519c13f8b77d6649caf4b8fa5858a731a5301c8 Mon Sep 17 00:00:00 2001 From: Marius Barbulescu Date: Wed, 5 Aug 2026 20:19:44 +0200 Subject: [PATCH 13/13] set copyright year to 2026 from 2025 --- .../org/openrewrite/java/spring/ConvertPropertiesToYaml.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java b/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java index f899291bf..24ccd9cd9 100644 --- a/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java +++ b/src/main/java/org/openrewrite/java/spring/ConvertPropertiesToYaml.java @@ -1,5 +1,5 @@ /* - * Copyright 2025 the original author or authors. + * Copyright 2026 the original author or authors. *

* Licensed under the Moderne Source Available License (the "License"); * you may not use this file except in compliance with the License.