Skip to content

Commit 6e1b2ed

Browse files
ishish07timtebeekgithub-actions[bot]
authored
Separating application.properties into different files based on profile (#563)
* First draft of changes * Added test to ensure application.properties doesn't change if no additional profiles are specified * Used Auto Formatter * More Formatting * More Formatting 2 * fixing edge case 1 * unable to append to existing application-prof.properties * testing for no application.properties * removed cycles from tests * This code works! * Forgot to format test file * More formatting * Fixed weird spacing issue * Formatting and Code Cleanup * Changed approach to create blank properties files and append to everything in order to simplify code * Converted existingPropertiesFiles to Set of Strings * New properties files will go into same folder as application.properties Finds properties files that are within folders * Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Add missing braces, language hints and apply formatter * Minor polish * Add test showing multi module project structure * Works for multi-modular projects * responding to bot's comment * using JavaProject * Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Move test class & method * Fix compilation * Use Paths in Accumulator object * Minimize * Final polish * Add to best practices --------- Co-authored-by: Tim te Beek <tim@moderne.io> Co-authored-by: Tim te Beek <timtebeek@gmail.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 5b623ca commit 6e1b2ed

File tree

3 files changed

+471
-0
lines changed

3 files changed

+471
-0
lines changed
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
* <p>
4+
* Licensed under the Moderne Source Available License (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://docs.moderne.io/licensing/moderne-source-available-license
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.java.spring;
17+
18+
import lombok.EqualsAndHashCode;
19+
import lombok.Value;
20+
import org.jspecify.annotations.Nullable;
21+
import org.openrewrite.*;
22+
import org.openrewrite.internal.ListUtils;
23+
import org.openrewrite.java.marker.JavaProject;
24+
import org.openrewrite.marker.Markers;
25+
import org.openrewrite.properties.PropertiesParser;
26+
import org.openrewrite.properties.PropertiesVisitor;
27+
import org.openrewrite.properties.tree.Properties;
28+
29+
import java.nio.file.Path;
30+
import java.util.*;
31+
import java.util.function.Predicate;
32+
33+
import static java.util.Collections.singletonList;
34+
import static java.util.stream.Collectors.toList;
35+
import static org.openrewrite.properties.tree.Properties.Comment.Delimiter.EXCLAMATION_MARK;
36+
import static org.openrewrite.properties.tree.Properties.Comment.Delimiter.HASH_TAG;
37+
38+
@Value
39+
@EqualsAndHashCode(callSuper = false)
40+
public class SeparateApplicationPropertiesByProfile extends ScanningRecipe<SeparateApplicationPropertiesByProfile.Accumulator> {
41+
42+
@Override
43+
public String getDisplayName() {
44+
return "Separate `application.properties` by profile";
45+
}
46+
47+
@Override
48+
public String getDescription() {
49+
return "Separating `application.properties` into separate files based on profiles.";
50+
}
51+
52+
@Override
53+
public Accumulator getInitialValue(ExecutionContext ctx) {
54+
return new Accumulator();
55+
}
56+
57+
@Override
58+
public TreeVisitor<?, ExecutionContext> getScanner(Accumulator acc) {
59+
return new TreeVisitor<Tree, ExecutionContext>() {
60+
@Override
61+
public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) {
62+
if (!(tree instanceof Properties.File)) {
63+
return tree;
64+
}
65+
66+
Properties.File propertyFile = (Properties.File) tree;
67+
Optional<JavaProject> javaProject = propertyFile.getMarkers().findFirst(JavaProject.class);
68+
if (!javaProject.isPresent()) {
69+
return tree;
70+
}
71+
72+
// Get or create the module info using the JavaProject marker as the key
73+
ModulePropertyInfo moduleInfo = acc.moduleProperties.computeIfAbsent(javaProject.get(), k -> new ModulePropertyInfo());
74+
if (propertyFile.getSourcePath().endsWith("application.properties")) {
75+
moduleInfo.extractedProfileProperties = extractPropertiesPerProfile(propertyFile);
76+
} else if (propertyFile.getSourcePath().getFileName().toString().matches("application-[^/]+\\.properties")) {
77+
moduleInfo.existingProfileProperties.add(propertyFile.getSourcePath());
78+
}
79+
return tree;
80+
}
81+
};
82+
}
83+
84+
85+
@Override
86+
public Collection<? extends SourceFile> generate(Accumulator acc, ExecutionContext ctx) {
87+
Set<SourceFile> newSourceFiles = new HashSet<>();
88+
PropertiesParser propertiesParser = PropertiesParser.builder().build();
89+
for (Map.Entry<JavaProject, ModulePropertyInfo> entry : acc.moduleProperties.entrySet()) {
90+
JavaProject javaProject = entry.getKey();
91+
ModulePropertyInfo moduleInfo = entry.getValue();
92+
for (Path fileToCreate : moduleInfo.extractedProfileProperties.keySet()) {
93+
if (!moduleInfo.existingProfileProperties.contains(fileToCreate)) {
94+
newSourceFiles.addAll(propertiesParser.parse("")
95+
.map(brandNewFile -> (SourceFile) brandNewFile.withSourcePath(fileToCreate)
96+
.withMarkers(Markers.build(singletonList(javaProject))))
97+
.collect(toList()));
98+
}
99+
}
100+
}
101+
return newSourceFiles;
102+
}
103+
104+
@Override
105+
public TreeVisitor<?, ExecutionContext> getVisitor(Accumulator acc) {
106+
return new PropertiesVisitor<ExecutionContext>() {
107+
@Override
108+
public Properties visitFile(Properties.File file, ExecutionContext ctx) {
109+
Optional<JavaProject> javaProject = file.getMarkers().findFirst(JavaProject.class);
110+
if (!javaProject.isPresent()) {
111+
return file;
112+
}
113+
114+
ModulePropertyInfo moduleInfo = acc.moduleProperties.get(javaProject.get());
115+
if (moduleInfo == null) {
116+
return file;
117+
}
118+
119+
if (file.getSourcePath().endsWith("application.properties")) {
120+
// Remove profile-specific sections
121+
return file.withContent(ListUtils.filter(file.getContent(), new Predicate<Properties.Content>() {
122+
boolean beforeSeparator = true;
123+
124+
@Override
125+
public boolean test(Properties.Content c) {
126+
if (isSeparator(c)) {
127+
beforeSeparator = false;
128+
}
129+
return beforeSeparator;
130+
}
131+
}));
132+
}
133+
134+
// Append extracted content to (now) existing profile-specific files
135+
return file.withContent(ListUtils.concatAll(file.getContent(),
136+
moduleInfo.extractedProfileProperties.get(file.getSourcePath())));
137+
}
138+
};
139+
}
140+
141+
private Map<Path, List<Properties.Content>> extractPropertiesPerProfile(Properties.File propertyFile) {
142+
Path applicationProperties = propertyFile.getSourcePath();
143+
List<Properties.Content> contentList = propertyFile.getContent();
144+
145+
Map<Path, List<Properties.Content>> map = new HashMap<>();
146+
int index = 0;
147+
while (index < contentList.size()) {
148+
if (isSeparator(contentList.get(index))) {
149+
List<Properties.Content> newContent = extractProfileContent(contentList, ++index);
150+
if (!newContent.isEmpty() && newContent.get(0) instanceof Properties.Entry) {
151+
String profileName = ((Properties.Entry) newContent.get(0)).getValue().getText();
152+
map.put(applicationProperties.resolveSibling(String.format("application-%s.properties", profileName)),
153+
newContent.subList(1, newContent.size()));
154+
}
155+
}
156+
index++;
157+
}
158+
return map;
159+
}
160+
161+
private List<Properties.Content> extractProfileContent(List<Properties.Content> contentList, int index) {
162+
List<Properties.Content> list = new ArrayList<>();
163+
while (index < contentList.size() && !isSeparator(contentList.get(index))) {
164+
if (contentList.get(index) instanceof Properties.Entry &&
165+
"spring.config.activate.on-profile".equals(((Properties.Entry) contentList.get(index)).getKey())) {
166+
list.add(0, contentList.get(index));
167+
} else {
168+
list.add(contentList.get(index));
169+
}
170+
index++;
171+
}
172+
return list;
173+
}
174+
175+
private boolean isSeparator(Properties.Content c) {
176+
return c instanceof Properties.Comment &&
177+
"---".equals(((Properties.Comment) c).getMessage()) &&
178+
((((Properties.Comment) c).getDelimiter() == HASH_TAG) ||
179+
((Properties.Comment) c).getDelimiter() == EXCLAMATION_MARK);
180+
}
181+
182+
public static class Accumulator {
183+
// Map from a module's JavaProject marker to its property file info
184+
Map<JavaProject, ModulePropertyInfo> moduleProperties = new HashMap<>();
185+
}
186+
187+
public static class ModulePropertyInfo {
188+
Set<Path> existingProfileProperties = new HashSet<>();
189+
Map<Path, List<Properties.Content>> extractedProfileProperties = new HashMap<>();
190+
}
191+
}

src/main/resources/META-INF/rewrite/best-practices.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ recipeList:
3131
- org.openrewrite.java.spring.NoAutowiredOnConstructor
3232
- org.openrewrite.java.spring.boot2.RestTemplateBuilderRequestFactory
3333
- org.openrewrite.java.spring.boot2.ReplaceDeprecatedEnvironmentTestUtils
34+
- org.openrewrite.java.spring.SeparateApplicationPropertiesByProfile
35+
- org.openrewrite.java.spring.SeparateApplicationYamlByProfile
3436

3537
---
3638
type: specs.openrewrite.org/v1beta/recipe

0 commit comments

Comments
 (0)