Skip to content
Open
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
1b36230
First draft of changes
ishish07 Aug 11, 2024
cb8dbe4
Added test to ensure application.properties doesn't change if no addi…
ishish07 Aug 11, 2024
ad78269
Used Auto Formatter
ishish07 Aug 11, 2024
75673fa
More Formatting
ishish07 Aug 11, 2024
27b2398
More Formatting 2
ishish07 Aug 11, 2024
b390d3a
fixing edge case 1
ishish07 Aug 12, 2024
26f3c19
unable to append to existing application-prof.properties
ishish07 Aug 12, 2024
e1eb520
testing for no application.properties
ishish07 Aug 12, 2024
b8c6239
removed cycles from tests
ishish07 Aug 12, 2024
cce5019
This code works!
ishish07 Aug 13, 2024
780250e
Forgot to format test file
ishish07 Aug 13, 2024
b1e4719
More formatting
ishish07 Aug 13, 2024
5b65012
Fixed weird spacing issue
ishish07 Aug 13, 2024
0cf74f9
Formatting and Code Cleanup
ishish07 Aug 13, 2024
e7398d3
Changed approach to create blank properties files and append to every…
ishish07 Aug 13, 2024
35ae8c7
Converted existingPropertiesFiles to Set of Strings
ishish07 Aug 13, 2024
825d68c
New properties files will go into same folder as application.properties
ishish07 Aug 23, 2024
46d75d0
Merge branch 'main' into feature/SeparateApplicationProperties
timtebeek Sep 11, 2024
5aef7ae
Apply suggestions from code review
timtebeek Sep 11, 2024
87e7146
Add missing braces, language hints and apply formatter
timtebeek Sep 11, 2024
6c3cdd7
Minor polish
timtebeek Sep 11, 2024
4f23297
Add test showing multi module project structure
timtebeek Sep 21, 2024
fbca498
Merge branch 'main' into feature/SeparateApplicationProperties
timtebeek Sep 21, 2024
9363b0f
Merge branch 'openrewrite:main' into feature/SeparateApplicationPrope…
ishish07 Jul 8, 2025
3631135
Merge branch 'openrewrite:main' into feature/SeparateApplicationPrope…
ishish07 Jul 8, 2025
798caa3
Works for multi-modular projects
ishish07 Jul 8, 2025
ef99fbb
responding to bot's comment
ishish07 Jul 9, 2025
85a9aa1
Merge branch 'openrewrite:main' into feature/SeparateApplicationPrope…
ishish07 Aug 1, 2025
dc7d1de
Merge branch 'openrewrite:main' into feature/SeparateApplicationPrope…
ishish07 Aug 4, 2025
967e809
using JavaProject
ishish07 Aug 5, 2025
c90923d
Merge branch 'main' into feature/SeparateApplicationProperties
timtebeek Aug 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
/*
* Copyright 2024 the original author or authors.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.properties.CreatePropertiesFile;
import org.openrewrite.properties.PropertiesVisitor;
import org.openrewrite.properties.tree.Properties;

import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import java.util.stream.Collectors;
import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.toList;

import java.util.stream.Stream;

@Value
@EqualsAndHashCode(callSuper = false)
public class SeparateApplicationPropertiesByProfile extends ScanningRecipe<SeparateApplicationPropertiesByProfile.Accumulator> {

@Override
public String getDisplayName() {
return "Separate `application.properties` by profile";
}

@Override
public String getDescription() {
return "Separating `application.properties` into separate files based on profiles.";
}

@Override
public Accumulator getInitialValue(ExecutionContext ctx) {
return new Accumulator();
}

@Override
public TreeVisitor<?, ExecutionContext> getScanner(Accumulator acc) {
return new TreeVisitor<Tree, ExecutionContext>() {
@Override
public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) {
if (!(tree instanceof Properties.File)) {
return tree;
}

Properties.File propertyFile = (Properties.File) tree;
String sourcePath = PathUtils.separatorsToUnix(propertyFile.getSourcePath().toString());
String[] pathArray = sourcePath.split("/");

// Find the module path by looking for src/main/resources
String modulePath = findModulePath(sourcePath);

// Get or create the module info
ModulePropertyInfo moduleInfo = acc.moduleProperties.computeIfAbsent(
modulePath,
k -> new ModulePropertyInfo()
);

if (propertyFile.getSourcePath().endsWith("application.properties")) {
moduleInfo.pathToApplicationProperties = getPathToApplicationProperties(pathArray);
moduleInfo.propertyFileContent = getNewApplicationPropertyFileInfo(propertyFile.getContent());
}

if (propertyFile.getSourcePath().getFileName().toString().matches("application-[^/]+\\.properties")) {
moduleInfo.fileNameToFilePath.put(pathArray[pathArray.length - 1], sourcePath);
}

return tree;
}

private String findModulePath(String sourcePath) {
// This finds the path up to src/main/resources
String[] parts = sourcePath.split("/");
for (int i = 0; i < parts.length; i++) {
if (parts[i].equals("src") &&
i + 2 < parts.length &&
parts[i+1].equals("main") &&
parts[i+2].equals("resources")) {
return String.join("/", Arrays.copyOfRange(parts, 0, i));
}
}
return ""; // fallback
}
};
}

@Override
public Collection<? extends SourceFile> generate(Accumulator acc, ExecutionContext ctx) {
Set<SourceFile> newApplicationPropertiesFiles = new HashSet<>();

for (ModulePropertyInfo moduleInfo : acc.moduleProperties.values()) {
if (moduleInfo.propertyFileContent.isEmpty()) {
continue;
}

for (Map.Entry<String, List<Properties.Content>> entry : moduleInfo.propertyFileContent.entrySet()) {
if (!moduleInfo.fileNameToFilePath.containsKey(entry.getKey())) {
newApplicationPropertiesFiles.add(
new CreatePropertiesFile(
moduleInfo.pathToApplicationProperties + entry.getKey(),
"",
null
).generate(new AtomicBoolean(true), ctx).iterator().next()
);
}
}
}

return newApplicationPropertiesFiles;
}

@Override
public TreeVisitor<?, ExecutionContext> getVisitor(Accumulator acc) {
return new PropertiesVisitor<ExecutionContext>() {
@Override
public Properties visitFile(Properties.File file, ExecutionContext ctx) {
String sourcePath = PathUtils.separatorsToUnix(file.getSourcePath().toString());
String modulePath = findModulePath(sourcePath);

ModulePropertyInfo moduleInfo = acc.moduleProperties.get(modulePath);
if (moduleInfo == null || moduleInfo.propertyFileContent.isEmpty()) {
return file;
}

String[] filePathArray = file.getSourcePath().toString().split("/");
String fileName = filePathArray[filePathArray.length - 1];

return fileName.matches("application.properties") ?
deleteFromApplicationProperties(file) :
appendToExistingPropertiesFile(file, moduleInfo.propertyFileContent.get(fileName));
}

private String findModulePath(String sourcePath) {
// Same implementation as in scanner
String[] parts = sourcePath.split("/");
for (int i = 0; i < parts.length; i++) {
if (parts[i].equals("src") &&
i + 2 < parts.length &&
parts[i+1].equals("main") &&
parts[i+2].equals("resources")) {
return String.join("/", Arrays.copyOfRange(parts, 0, i));
}
}
return ""; // fallback
}
};
}

private Properties appendToExistingPropertiesFile(Properties.File file, List<Properties.Content> contentToAppend) {
return file.withContent(
Stream.concat(file.getContent().stream(), contentToAppend.stream()).
collect(Collectors.toList()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
collect(Collectors.toList()));
collect(toList()));

}

private Properties deleteFromApplicationProperties(Properties.File applicationProperties) {
List<Properties.Content> newContent = new ArrayList<>();
for (Properties.Content c : applicationProperties.getContent()) {
if (isSeparator(c)) {
break;
}
newContent.add(c);
}
return applicationProperties.getContent().equals(newContent) ? applicationProperties :
applicationProperties.withContent(newContent);
}

private Map<String, List<Properties.Content>> getNewApplicationPropertyFileInfo(List<Properties.Content> contentList) {
Map<String, List<Properties.Content>> map = new HashMap<>();
int index = 0;
while (index < contentList.size()) {
if (isSeparator(contentList.get(index))) {
List<Properties.Content> newContent = getContentForNewFile(contentList, ++index);
map.put("application-" + ((Properties.Entry) newContent.get(0)).getValue().getText() + ".properties",
newContent.subList(1, newContent.size()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should use the full target file name as key here, since there could be a multi module project that could clash.

parent
  - childA
    - src/main/resource/application.properties
  - childB
    - src/main/resource/application.properties

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the code already adjusts for files within nested folders like your example (line 206 of the testing file). I also do not think that a project can have multiple application.properties files.

The method above - getNewApplicationPropertyFileInfo is simply for getting the content of each application-env.properties file from application.properties. Application-env.properties files that don't exist will, by default, be placed in the same folder as the application.properties file.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi! I've just pushed a test to highlight what I meant; 4f23297
The test at line 206 only had a single folder structure, not two separate.

Given the scale at which recipes can run I think we should account for such a scenario.

}
index++;
}
return map;
}

private List<Properties.Content> getContentForNewFile(List<Properties.Content> contentList, int index) {
List<Properties.Content> list = new ArrayList<>();
while (index < contentList.size() && !isSeparator(contentList.get(index))) {
if (contentList.get(index) instanceof Properties.Entry &&
((Properties.Entry) contentList.get(index)).getKey().equals
("spring.config.activate.on-profile")) {
list.add(0, contentList.get(index));
} else {
list.add(contentList.get(index));
}
index++;
}
return list;
}

private String getPathToApplicationProperties(String[] pathArray) {
return pathArray.length == 1 ? "" : String.join("/", Arrays.copyOfRange(pathArray, 0, pathArray.length - 1)) + "/";
}

private boolean isSeparator(Properties.Content c) {
return c instanceof Properties.Comment &&
((Properties.Comment) c).getMessage().equals("---") &&
((((Properties.Comment) c).getDelimiter().equals(Properties.Comment.Delimiter.valueOf("HASH_TAG"))) ||
((Properties.Comment) c).getDelimiter().equals(Properties.Comment.Delimiter.valueOf("EXCLAMATION_MARK")));
}

public static class Accumulator {
// Map from module path to its property file info
Map<String, ModulePropertyInfo> moduleProperties = new HashMap<>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making a quick note for myself here: Elsewhere we use Map<JavaProject, ....> when working with scanning recipes across multi modules projects, and use the JavaProject marker that's available as opposed to a String key. Welcome to adjust to using that already, or we can try to fit that in with a review.

}


public static class ModulePropertyInfo {
String pathToApplicationProperties = "";
Map<String, String> fileNameToFilePath = new HashMap<>();
Map<String, List<Properties.Content>> propertyFileContent = new HashMap<>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Map<String, List<Properties.Content>> propertyFileContent = new HashMap<>();
Map<String, List<Properties.Content>> propertyFileContent = new HashMap<>();

}
}
Loading