Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separating application.properties into different files based on profile #563

Draft
wants to merge 23 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
23 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
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,179 @@
/*
* 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;
timtebeek marked this conversation as resolved.
Show resolved Hide resolved

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;
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 while appending to any existing `application-profile.properties`.";
}

@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("/");

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

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

return tree;
}
};
}

@Override
public Collection<? extends SourceFile> generate(Accumulator acc, ExecutionContext ctx) {
if (acc.propertyFileContent.isEmpty()) {
return Collections.emptyList();
}

Set<SourceFile> newApplicationPropertiesFiles = new HashSet<>();

for (Map.Entry<String, List<Properties.Content>> entry : acc.propertyFileContent.entrySet()) {
if (!acc.fileNameToFilePath.containsKey(entry.getKey())) {
newApplicationPropertiesFiles.
add(new CreatePropertiesFile(acc.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) {
if (acc.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, acc.propertyFileContent.get(fileName));
}
};
}

private Properties appendToExistingPropertiesFile(Properties.File file, List<Properties.Content> contentToAppend) {
return file.withContent(
Stream.concat(file.getContent().stream(), contentToAppend.stream()).
collect(Collectors.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()));
Comment on lines +140 to +141
Copy link
Contributor

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
Contributor

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 {
String pathToApplicationProperties = "";
Map<String, String> fileNameToFilePath = new HashMap<>();
Map<String, List<Properties.Content>> propertyFileContent = new HashMap<>();
}
}
Loading
Loading