diff --git a/src/main/java/org/openrewrite/java/spring/SeparateApplicationPropertiesByProfile.java b/src/main/java/org/openrewrite/java/spring/SeparateApplicationPropertiesByProfile.java new file mode 100644 index 000000000..353f820c1 --- /dev/null +++ b/src/main/java/org/openrewrite/java/spring/SeparateApplicationPropertiesByProfile.java @@ -0,0 +1,237 @@ +/* + * Copyright 2024 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.openrewrite.java.spring; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.jspecify.annotations.Nullable; +import org.openrewrite.*; +import org.openrewrite.java.marker.JavaProject; +import org.openrewrite.marker.Markers; +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 { + + @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 getScanner(Accumulator acc) { + return new TreeVisitor() { + @Override + public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) { + if (!(tree instanceof Properties.File)) { + return tree; + } + + Properties.File propertyFile = (Properties.File) tree; + Optional javaProject = propertyFile.getMarkers().findFirst(JavaProject.class); + if (!javaProject.isPresent()) { + return tree; + } + + String sourcePath = PathUtils.separatorsToUnix(propertyFile.getSourcePath().toString()); + String[] pathArray = sourcePath.split("/"); + + // Get or create the module info using the JavaProject marker as the key + ModulePropertyInfo moduleInfo = acc.moduleProperties.computeIfAbsent( + javaProject.get(), + k -> new ModulePropertyInfo() + ); + + if (moduleInfo.javaProject == null) { + moduleInfo.javaProject = javaProject.get(); + } + + 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; + } + }; + } + + + + @Override + public Collection generate(Accumulator acc, ExecutionContext ctx) { + Set newApplicationPropertiesFiles = new HashSet<>(); + + // Change the loop to iterate over the entrySet to access the full module info + for (ModulePropertyInfo moduleInfo : acc.moduleProperties.values()) { + if (moduleInfo.propertyFileContent.isEmpty() || moduleInfo.javaProject == null) { + continue; + } + + for (Map.Entry> entry : moduleInfo.propertyFileContent.entrySet()) { + if (!moduleInfo.fileNameToFilePath.containsKey(entry.getKey())) { + + // 1. Generate the new file as before + SourceFile newFile = new CreatePropertiesFile( + moduleInfo.pathToApplicationProperties + entry.getKey(), + "", + null + ).generate(new AtomicBoolean(true), ctx).iterator().next(); + + // 2. Get the stored project marker + JavaProject projectMarker = moduleInfo.javaProject; + + // 3. Use withMarkers() to attach it to the new file + SourceFile newFileWithMarker = newFile.withMarkers( + // Markers.build() creates the container for our marker + Markers.build(Collections.singletonList(projectMarker)) + ); + + // 4. Add the file *with the new marker* to our results + newApplicationPropertiesFiles.add(newFileWithMarker); + } + } + } + + return newApplicationPropertiesFiles; + } + + @Override + public TreeVisitor getVisitor(Accumulator acc) { + return new PropertiesVisitor() { + @Override + public Properties visitFile(Properties.File file, ExecutionContext ctx) { + Optional javaProject = file.getMarkers().findFirst(JavaProject.class); + if (!javaProject.isPresent()) { + return file; + } + + ModulePropertyInfo moduleInfo = acc.moduleProperties.get(javaProject.get()); + 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 Properties appendToExistingPropertiesFile(Properties.File file, List contentToAppend) { + return file.withContent( + Stream.concat(file.getContent().stream(), contentToAppend.stream()). + collect(Collectors.toList())); + } + + private Properties deleteFromApplicationProperties(Properties.File applicationProperties) { + List 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> getNewApplicationPropertyFileInfo(List contentList) { + Map> map = new HashMap<>(); + int index = 0; + while (index < contentList.size()) { + if (isSeparator(contentList.get(index))) { + List newContent = getContentForNewFile(contentList, ++index); + if (!newContent.isEmpty() && newContent.get(0) instanceof Properties.Entry) { + map.put("application-" + ((Properties.Entry) newContent.get(0)).getValue().getText() + ".properties", + newContent.subList(1, newContent.size())); + } + } + index++; + } + return map; + } + + private List getContentForNewFile(List contentList, int index) { + List 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() == + Properties.Comment.Delimiter.valueOf("HASH_TAG")) || + + ((Properties.Comment) c).getDelimiter() == + Properties.Comment.Delimiter.valueOf("EXCLAMATION_MARK")); + } + + public static class Accumulator { + // Map from a module's JavaProject marker to its property file info + Map moduleProperties = new HashMap<>(); + } + + + public static class ModulePropertyInfo { + String pathToApplicationProperties = ""; + Map fileNameToFilePath = new HashMap<>(); + Map> propertyFileContent = new HashMap<>(); + @Nullable + JavaProject javaProject; + } +} diff --git a/src/testWithSpringBoot_2_4/java/org/openrewrite/java/spring/SeparateApplicationPropertiesByProfileTest.java b/src/testWithSpringBoot_2_4/java/org/openrewrite/java/spring/SeparateApplicationPropertiesByProfileTest.java new file mode 100644 index 000000000..19965d7bb --- /dev/null +++ b/src/testWithSpringBoot_2_4/java/org/openrewrite/java/spring/SeparateApplicationPropertiesByProfileTest.java @@ -0,0 +1,354 @@ +/* + * Copyright 2024 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.spring; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.mavenProject; +import static org.openrewrite.java.Assertions.srcMainResources; +import static org.openrewrite.properties.Assertions.properties; + +class SeparateApplicationPropertiesByProfileTest implements RewriteTest { + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new SeparateApplicationPropertiesByProfile()); + } + + @Test + void noApplicationProperties() { + rewriteRun( + mavenProject("parent", + srcMainResources( + properties( + //language=properties + """ + spring.application.name=Openrewrite-PR-Service + #PR-Service + + base-url.PR-services=http://my.url.com + exchange-token=1234567890 + exchange-tokens=${base-url.PR-services}/exchange-token + """, + sourceSpecs -> sourceSpecs.path("application-dev.properties")) + ) + ) + ); + } + + @Test + void noSeparateProfile() { + rewriteRun( + mavenProject("parent", + srcMainResources( + properties( + //language=properties + """ + spring.application.name=Openrewrite-PR-Service + #PR-Service + + base-url.PR-services=http://my.url.com + exchange-token=1234567890 + exchange-tokens=${base-url.PR-services}/exchange-token + """, + sourceSpecs -> sourceSpecs.path("application.properties")) + ) + ) + ); + } + + @DocumentExample + @Test + void separateProfileWithAppend() { + rewriteRun( + mavenProject("parent", + srcMainResources( + properties( + //language=properties + """ + line1=line1 + """, + //language=properties + """ + line1=line1 + oauth2.clientId=9999999999999999999999 + service.domainUrl= https://this.is.my.dev.url.com + app.config.currentEnvironment=DEV + """, + sourceSpecs -> sourceSpecs.path("application-dev.properties") + ), + properties( + //language=properties + """ + spring.application.name=Openrewrite-PR-Service + #PR-Service + base-url.PR-services=http://my.url.com + exchange-token=1234567890 + exchange-tokens=${base-url.PR-services}/exchange-token + !--- + spring.config.activate.on-profile=dev + oauth2.clientId=9999999999999999999999 + service.domainUrl= https://this.is.my.dev.url.com + app.config.currentEnvironment=DEV + #--- + spring.config.activate.on-profile=local + app.config.currentEnvironment=LOCAL + + + #--- + #### XX Configuration #### + spring.config.activate.on-profile=prod + oauth2.clientId=77777777777777 + service.domainUrl=https://this.is.my.prod.url.com + """, + //language=properties + """ + spring.application.name=Openrewrite-PR-Service + #PR-Service + base-url.PR-services=http://my.url.com + exchange-token=1234567890 + exchange-tokens=${base-url.PR-services}/exchange-token + """, + sourceSpecs -> sourceSpecs.path("application.properties") + ), + properties( + null, + """ + app.config.currentEnvironment=LOCAL + """, + sourceSpecs -> sourceSpecs.path("application-local.properties") + ), + properties( + null, + //language=properties + """ + #### XX Configuration #### + oauth2.clientId=77777777777777 + service.domainUrl=https://this.is.my.prod.url.com + """, + sourceSpecs -> sourceSpecs.path("application-prod.properties") + ) + ) + ) + ); + } + + @Test + void separateProfileWithoutAppend() { + rewriteRun( + mavenProject("parent", + srcMainResources( + properties( + null, + //language=properties + """ + oauth2.clientId=9999999999999999999999 + service.domainUrl= https://this.is.my.dev.url.com + app.config.currentEnvironment=DEV + """, + sourceSpecs -> sourceSpecs.path("application-dev.properties") + ), + properties( + //language=properties + """ + spring.application.name=Openrewrite-PR-Service + #PR-Service + base-url.PR-services=http://my.url.com + exchange-token=1234567890 + exchange-tokens=${base-url.PR-services}/exchange-token + !--- + spring.config.activate.on-profile=dev + oauth2.clientId=9999999999999999999999 + service.domainUrl= https://this.is.my.dev.url.com + app.config.currentEnvironment=DEV + #--- + spring.config.activate.on-profile=local + app.config.currentEnvironment=LOCAL + + + #--- + #### XX Configuration #### + spring.config.activate.on-profile=prod + oauth2.clientId=77777777777777 + service.domainUrl=https://this.is.my.prod.url.com + """, + //language=properties + """ + spring.application.name=Openrewrite-PR-Service + #PR-Service + base-url.PR-services=http://my.url.com + exchange-token=1234567890 + exchange-tokens=${base-url.PR-services}/exchange-token + """, + sourceSpecs -> sourceSpecs.path("application.properties") + ), + properties( + null, + //language=properties + """ + app.config.currentEnvironment=LOCAL + """, + sourceSpecs -> sourceSpecs.path("application-local.properties") + ), + properties( + null, + //language=properties + """ + #### XX Configuration #### + oauth2.clientId=77777777777777 + service.domainUrl=https://this.is.my.prod.url.com + """, + sourceSpecs -> sourceSpecs.path("application-prod.properties") + ) + ) + ) + ); + } + + @Test + void pathToApplicationProperties() { + rewriteRun( + mavenProject("parent", + srcMainResources( + properties( + //language=properties + """ + line1=line1 + """, + //language=properties + """ + line1=line1 + oauth2.clientId=9999999999999999999999 + service.domainUrl= https://this.is.my.dev.url.com + app.config.currentEnvironment=DEV + """, + sourceSpecs -> sourceSpecs.path("folder1/folder2/application-dev.properties") + ), + properties( + //language=properties + """ + spring.application.name=Openrewrite-PR-Service + #PR-Service + base-url.PR-services=http://my.url.com + exchange-token=1234567890 + exchange-tokens=${base-url.PR-services}/exchange-token + !--- + spring.config.activate.on-profile=dev + oauth2.clientId=9999999999999999999999 + service.domainUrl= https://this.is.my.dev.url.com + app.config.currentEnvironment=DEV + #--- + spring.config.activate.on-profile=local + app.config.currentEnvironment=LOCAL + + + #--- + #### XX Configuration #### + spring.config.activate.on-profile=prod + oauth2.clientId=77777777777777 + service.domainUrl=https://this.is.my.prod.url.com + """, + //language=properties + """ + spring.application.name=Openrewrite-PR-Service + #PR-Service + base-url.PR-services=http://my.url.com + exchange-token=1234567890 + exchange-tokens=${base-url.PR-services}/exchange-token + """, + sourceSpecs -> sourceSpecs.path("folder1/folder2/application.properties") + ), + properties( + null, + //language=properties + """ + app.config.currentEnvironment=LOCAL + """, + sourceSpecs -> sourceSpecs.path("folder1/folder2/application-local.properties") + ), + properties( + null, + //language=properties + """ + #### XX Configuration #### + oauth2.clientId=77777777777777 + service.domainUrl=https://this.is.my.prod.url.com + """, + sourceSpecs -> sourceSpecs.path("folder1/folder2/application-prod.properties") + ) + ) + ) + ); + } + + @Test + void multiModuleProject() { + rewriteRun( + mavenProject("parent", + mavenProject("service", + srcMainResources( + properties( + """ + global.service=true + !--- + spring.config.activate.on-profile=dev + dev.service=true + """, + """ + global.service=true + """, + spec -> spec.path("application.properties") + ), + properties( + null, + """ + dev.service=true + """, + spec -> spec.path("application-dev.properties") + ) + ) + ), + mavenProject("client", + srcMainResources( + properties(""" + global.client=true + !--- + spring.config.activate.on-profile=dev + dev.client=true + """, + """ + global.client=true + """, + spec -> spec.path("application.properties") + ), + properties( + """ + dev.existing=true + """, + """ + dev.existing=true + dev.client=true + """, + spec -> spec.path("application-dev.properties") + ) + ) + ) + ) + ); + } +}