diff --git a/rewrite-gradle/src/main/java/org/openrewrite/gradle/AddProperty.java b/rewrite-gradle/src/main/java/org/openrewrite/gradle/AddProperty.java index 5774fa1f7a..38fbaf4af8 100644 --- a/rewrite-gradle/src/main/java/org/openrewrite/gradle/AddProperty.java +++ b/rewrite-gradle/src/main/java/org/openrewrite/gradle/AddProperty.java @@ -124,7 +124,7 @@ public Tree visit(@Nullable Tree tree, ExecutionContext ctx) { sourceFile : new ChangePropertyValue(key, value, null, false, null) .getVisitor().visitNonNull(sourceFile, ctx); - return new org.openrewrite.properties.AddProperty(key, value, null, null, null) + return new org.openrewrite.properties.AddProperty(key, value, null, null, null, null, null) .getVisitor() .visitNonNull(t, ctx); } @@ -133,7 +133,7 @@ public Tree visit(@Nullable Tree tree, ExecutionContext ctx) { sourceFile : new ChangePropertyValue(key, value, null, false, null) .getVisitor().visitNonNull(sourceFile, ctx); - return new org.openrewrite.properties.AddProperty(key, value, null, null, null) + return new org.openrewrite.properties.AddProperty(key, value, null, null, null, null, null) .getVisitor() .visitNonNull(t, ctx); } diff --git a/rewrite-properties/src/main/java/org/openrewrite/properties/AddProperty.java b/rewrite-properties/src/main/java/org/openrewrite/properties/AddProperty.java index 3d74d12d22..0f5a38361e 100644 --- a/rewrite-properties/src/main/java/org/openrewrite/properties/AddProperty.java +++ b/rewrite-properties/src/main/java/org/openrewrite/properties/AddProperty.java @@ -70,6 +70,26 @@ public class AddProperty extends Recipe { @Nullable Boolean orderedInsertion; + @Option(displayName = "Before property", + description = "Insert the new property before the property with this key. " + + "Takes precedence over `orderedInsertion`. " + + "If the referenced property does not exist, falls back to default behavior. " + + "Mutually exclusive with `afterProperty`.", + required = false, + example = "server.port") + @Nullable + String beforeProperty; + + @Option(displayName = "After property", + description = "Insert the new property after the property with this key. " + + "Takes precedence over `orderedInsertion`. " + + "If the referenced property does not exist, falls back to default behavior. " + + "Mutually exclusive with `beforeProperty`.", + required = false, + example = "server.port") + @Nullable + String afterProperty; + String displayName = "Add a new property"; String description = "Adds a new property to a property file. " + @@ -80,7 +100,11 @@ public class AddProperty extends Recipe { public Validated validate() { return Validated.none() .and(Validated.required("property", property)) - .and(Validated.required("value", value)); + .and(Validated.required("value", value)) + .and(Validated.test("beforeProperty", + "`beforeProperty` and `afterProperty` are mutually exclusive", + this, + r -> StringUtils.isBlank(r.beforeProperty) || StringUtils.isBlank(r.afterProperty))); } @Override @@ -117,16 +141,39 @@ public Properties.File visitFile(Properties.File file, ExecutionContext ctx) { entry); } - List contentList = new ArrayList<>(p.getContent().size() + 1); - if (orderedInsertion == null || orderedInsertion) { - int insertionIndex = sortedInsertionIndex(entry, p.getContent()); - contentList.addAll(p.getContent().subList(0, insertionIndex)); - contentList.addAll(newContents); - contentList.addAll(p.getContent().subList(insertionIndex, p.getContent().size())); + List contentList = new ArrayList<>(p.getContent().size() + newContents.size()); + boolean inserted = false; + + if (!StringUtils.isBlank(beforeProperty)) { + int refIndex = findEntryIndex(p.getContent(), p, beforeProperty); + if (refIndex >= 0) { + int insertIndex = findCommentBlockStart(p.getContent(), refIndex); + contentList.addAll(p.getContent().subList(0, insertIndex)); + contentList.addAll(newContents); + contentList.addAll(p.getContent().subList(insertIndex, p.getContent().size())); + inserted = true; + } + } else if (!StringUtils.isBlank(afterProperty)) { + int refIndex = findEntryIndex(p.getContent(), p, afterProperty); + if (refIndex >= 0) { + int insertIndex = refIndex + 1; + contentList.addAll(p.getContent().subList(0, insertIndex)); + contentList.addAll(newContents); + contentList.addAll(p.getContent().subList(insertIndex, p.getContent().size())); + inserted = true; + } } - else { - contentList.addAll(p.getContent()); - contentList.addAll(newContents); + + if (!inserted) { + if (orderedInsertion == null || orderedInsertion) { + int insertionIndex = sortedInsertionIndex(entry, p.getContent()); + contentList.addAll(p.getContent().subList(0, insertionIndex)); + contentList.addAll(newContents); + contentList.addAll(p.getContent().subList(insertionIndex, p.getContent().size())); + } else { + contentList.addAll(p.getContent()); + contentList.addAll(newContents); + } } @@ -145,6 +192,23 @@ public Properties.File visitFile(Properties.File file, ExecutionContext ctx) { }; } + private static int findEntryIndex(List contentList, Properties.File file, String referenceKey) { + Set matches = FindProperties.find(file, referenceKey, false); + if (matches.isEmpty()) { + return -1; + } + Properties.Entry matched = matches.iterator().next(); + return contentList.indexOf(matched); + } + + private static int findCommentBlockStart(List contentList, int entryIndex) { + int start = entryIndex; + while (start > 0 && contentList.get(start - 1) instanceof Properties.Comment) { + start--; + } + return start; + } + private static int sortedInsertionIndex(Properties.Entry entry, List contentsList) { if (contentsList.isEmpty()) { return 0; diff --git a/rewrite-properties/src/main/resources/META-INF/rewrite/examples.yml b/rewrite-properties/src/main/resources/META-INF/rewrite/examples.yml index 45d092c27e..1952b40988 100644 --- a/rewrite-properties/src/main/resources/META-INF/rewrite/examples.yml +++ b/rewrite-properties/src/main/resources/META-INF/rewrite/examples.yml @@ -22,6 +22,8 @@ examples: - 'null' - 'null' - 'null' + - 'null' + - 'null' sources: - before: | management=true diff --git a/rewrite-properties/src/main/resources/META-INF/rewrite/recipes.csv b/rewrite-properties/src/main/resources/META-INF/rewrite/recipes.csv index 0516ec05ac..4e9fe65689 100644 --- a/rewrite-properties/src/main/resources/META-INF/rewrite/recipes.csv +++ b/rewrite-properties/src/main/resources/META-INF/rewrite/recipes.csv @@ -1,5 +1,5 @@ ecosystem,packageName,name,displayName,description,recipeCount,category1,category2,options -maven,org.openrewrite:rewrite-properties,org.openrewrite.properties.AddProperty,Add a new property,Adds a new property to a property file. Attempts to place the new property in alphabetical order by the property keys. Whitespace before and after the `=` must be included in the property and value.,1,,Properties,"[{""name"":""property"",""type"":""String"",""displayName"":""Property key"",""description"":""The property key to add."",""example"":""management.metrics.enable.process.files"",""required"":true},{""name"":""value"",""type"":""String"",""displayName"":""Property value"",""description"":""The value of the new property key."",""example"":""newPropValue"",""required"":true},{""name"":""comment"",""type"":""String"",""displayName"":""Optional comment to be prepended to the property"",""description"":""A comment that will be added to the new property."",""example"":""This is a comment""},{""name"":""delimiter"",""type"":""String"",""displayName"":""Optional delimiter"",""description"":""Property entries support different delimiters (`=`, `:`, or whitespace). The default value is `=` unless provided the delimiter of the new property entry."",""example"":"":""},{""name"":""orderedInsertion"",""type"":""Boolean"",""displayName"":""Ordered property insertion"",""description"":""Whether to attempt adding the property in an order following alphabetic sorting. The default value is `true`."",""example"":""false""}]" +maven,org.openrewrite:rewrite-properties,org.openrewrite.properties.AddProperty,Add a new property,Adds a new property to a property file. Attempts to place the new property in alphabetical order by the property keys. Whitespace before and after the `=` must be included in the property and value.,1,,Properties,"[{""name"":""property"",""type"":""String"",""displayName"":""Property key"",""description"":""The property key to add."",""example"":""management.metrics.enable.process.files"",""required"":true},{""name"":""value"",""type"":""String"",""displayName"":""Property value"",""description"":""The value of the new property key."",""example"":""newPropValue"",""required"":true},{""name"":""comment"",""type"":""String"",""displayName"":""Optional comment to be prepended to the property"",""description"":""A comment that will be added to the new property."",""example"":""This is a comment""},{""name"":""delimiter"",""type"":""String"",""displayName"":""Optional delimiter"",""description"":""Property entries support different delimiters (`=`, `:`, or whitespace). The default value is `=` unless provided the delimiter of the new property entry."",""example"":"":""},{""name"":""orderedInsertion"",""type"":""Boolean"",""displayName"":""Ordered property insertion"",""description"":""Whether to attempt adding the property in an order following alphabetic sorting. The default value is `true`."",""example"":""false""},{""name"":""beforeProperty"",""type"":""String"",""displayName"":""Before property"",""description"":""Insert the new property before the property with this key. Takes precedence over `orderedInsertion`. If the referenced property does not exist, falls back to default behavior. Mutually exclusive with `afterProperty`."",""example"":""server.port""},{""name"":""afterProperty"",""type"":""String"",""displayName"":""After property"",""description"":""Insert the new property after the property with this key. Takes precedence over `orderedInsertion`. If the referenced property does not exist, falls back to default behavior. Mutually exclusive with `beforeProperty`."",""example"":""server.port""}]" maven,org.openrewrite:rewrite-properties,org.openrewrite.properties.AddPropertyComment,Add comment before property key,"Add a new comment before a property key if not already present, optionally commenting out the property.",1,,Properties,"[{""name"":""propertyKey"",""type"":""String"",""displayName"":""Property key"",""description"":""The name of the property to add comment."",""example"":""management.metrics.binders"",""required"":true},{""name"":""comment"",""type"":""String"",""displayName"":""Comment"",""description"":""The comment to be added."",""example"":""comment"",""required"":true},{""name"":""commentOutProperty"",""type"":""Boolean"",""displayName"":""Comment out property"",""description"":""If true, property will be commented out."",""example"":""true""}]" maven,org.openrewrite:rewrite-properties,org.openrewrite.properties.CopyValue,Copy property value,"Copies a property value from one key to another. The existing key/value pair remains unaffected by this change. If the destination key already exists, its value will be replaced. By default, creates the destination key if it does not exist.",1,,Properties,"[{""name"":""oldPropertyKey"",""type"":""String"",""displayName"":""Old property key"",""description"":""The property key to copy the value from. Supports glob patterns."",""example"":""app.source.property"",""required"":true},{""name"":""oldFilePath"",""type"":""String"",""displayName"":""Old file path"",""description"":""The file path to the properties file to copy the value from. If `null` then the value will be copied from any properties file it appears within."",""example"":""src/main/resources/application.properties""},{""name"":""newPropertyKey"",""type"":""String"",""displayName"":""New property key"",""description"":""The property key to copy the value to."",""example"":""app.destination.property"",""required"":true},{""name"":""newFilePath"",""type"":""String"",""displayName"":""New file path"",""description"":""The file path to the properties file to copy the value to. If `null` then the value will be copied only into the same file it was found in."",""example"":""src/main/resources/application.properties""},{""name"":""createNewKeys"",""type"":""Boolean"",""displayName"":""Create new keys"",""description"":""When the destination key does _not_ already exist, create it. Default is `true`.""},{""name"":""relaxedBinding"",""type"":""Boolean"",""displayName"":""Use relaxed binding"",""description"":""Whether to match the `oldPropertyKey` 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:rewrite-properties,org.openrewrite.properties.ChangePropertyKey,Change property key,Change a property key leaving the value intact.,1,,Properties,"[{""name"":""oldPropertyKey"",""type"":""String"",""displayName"":""Old property key"",""description"":""The property key to rename."",""example"":""management.metrics.binders.files.enabled"",""required"":true},{""name"":""newPropertyKey"",""type"":""String"",""displayName"":""New property key"",""description"":""The new name for the key identified by `oldPropertyKey`."",""example"":""management.metrics.enable.process.files"",""required"":true},{""name"":""relaxedBinding"",""type"":""Boolean"",""displayName"":""Use relaxed binding"",""description"":""Whether to match the `oldPropertyKey` 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.""},{""name"":""regex"",""type"":""Boolean"",""displayName"":""Regex"",""description"":""Default false. If enabled, `oldPropertyKey` will be interpreted as a Regular Expression, and capture group contents will be available in `newPropertyKey`""}]" diff --git a/rewrite-properties/src/test/java/org/openrewrite/properties/AddPropertyTest.java b/rewrite-properties/src/test/java/org/openrewrite/properties/AddPropertyTest.java index 57cab16f70..b2b4ac023d 100755 --- a/rewrite-properties/src/test/java/org/openrewrite/properties/AddPropertyTest.java +++ b/rewrite-properties/src/test/java/org/openrewrite/properties/AddPropertyTest.java @@ -18,6 +18,7 @@ import org.junit.jupiter.api.Test; import org.openrewrite.DocumentExample; import org.openrewrite.Issue; +import org.openrewrite.Validated; import org.openrewrite.properties.tree.Properties; import org.openrewrite.test.RewriteTest; @@ -36,6 +37,8 @@ void newProperty() { "true", null, null, + null, + null, null )), properties( @@ -58,6 +61,8 @@ void emptyProperty() { "true", null, null, + null, + null, null )), properties( @@ -76,6 +81,8 @@ void emptyValue() { "", null, null, + null, + null, null )), properties( @@ -94,6 +101,8 @@ void containsProperty() { "true", null, null, + null, + null, null )), properties( @@ -113,6 +122,8 @@ void delimitedByColon() { "true", null, ":", + null, + null, null )), properties( @@ -136,6 +147,8 @@ void delimitedByWhitespace() { "true", null, " ", + null, + null, null )), properties( @@ -158,6 +171,8 @@ void addToEmptyFile() { "true", null, null, + null, + null, null )), properties( @@ -177,6 +192,8 @@ void addCommentedPropertyToEmptyFile() { "true", "Management metrics", null, + null, + null, null )), properties( @@ -197,6 +214,8 @@ void addCommentedPropertyToExistingFile() { "true", "Management metrics", null, + null, + null, null )), properties( @@ -218,6 +237,8 @@ void keepPropertyValueWithLineContinuations() { "true", null, null, + null, + null, null )), properties( @@ -247,6 +268,8 @@ void orderedInsertionBeginning() { "true", "sam", null, + null, + null, null )), properties( @@ -270,6 +293,8 @@ void orderedInsertionMiddle() { "true", "sam", null, + null, + null, null )), properties( @@ -307,6 +332,8 @@ void orderedInsertionEnd() { "true", "sam", null, + null, + null, null )), properties( @@ -330,7 +357,9 @@ void unorderedInsertion() { "true", "sam", null, - false + false, + null, + null )), properties( """ @@ -359,4 +388,266 @@ void unorderedInsertion() { ); } + @Test + void beforePropertyBasic() { + rewriteRun( + spec -> spec.recipe(new AddProperty( + "new.prop", + "val", + null, + null, + null, + "com.bea", + null + )), + properties( + """ + com.amy=true + com.bea=true + com.zoe=true + """, + """ + com.amy=true + new.prop=val + com.bea=true + com.zoe=true + """ + ) + ); + } + + @Test + void afterPropertyBasic() { + rewriteRun( + spec -> spec.recipe(new AddProperty( + "new.prop", + "val", + null, + null, + null, + null, + "com.bea" + )), + properties( + """ + com.amy=true + com.bea=true + com.zoe=true + """, + """ + com.amy=true + com.bea=true + new.prop=val + com.zoe=true + """ + ) + ); + } + + @Test + void beforePropertyWithPrecedingComment() { + rewriteRun( + spec -> spec.recipe(new AddProperty( + "new.prop", + "val", + null, + null, + null, + "com.bea", + null + )), + properties( + """ + com.amy=true + # bea comment + com.bea=true + com.zoe=true + """, + """ + com.amy=true + new.prop=val + # bea comment + com.bea=true + com.zoe=true + """ + ) + ); + } + + @Test + void beforePropertyWithNewPropertyComment() { + rewriteRun( + spec -> spec.recipe(new AddProperty( + "new.prop", + "val", + "new property comment", + null, + null, + "com.bea", + null + )), + properties( + """ + com.amy=true + com.bea=true + com.zoe=true + """, + """ + com.amy=true + # new property comment + new.prop=val + com.bea=true + com.zoe=true + """ + ) + ); + } + + @Test + void afterPropertyWithNewPropertyComment() { + rewriteRun( + spec -> spec.recipe(new AddProperty( + "new.prop", + "val", + "new property comment", + null, + null, + null, + "com.bea" + )), + properties( + """ + com.amy=true + com.bea=true + com.zoe=true + """, + """ + com.amy=true + com.bea=true + # new property comment + new.prop=val + com.zoe=true + """ + ) + ); + } + + @Test + void beforePropertyNotFoundFallsBackToAlphabetical() { + rewriteRun( + spec -> spec.recipe(new AddProperty( + "com.sam", + "true", + null, + null, + null, + "nonexistent.key", + null + )), + properties( + """ + com.amy=true + com.zoe=true + """, + """ + com.amy=true + com.sam=true + com.zoe=true + """ + ) + ); + } + + @Test + void afterPropertyNotFoundUnordered() { + rewriteRun( + spec -> spec.recipe(new AddProperty( + "com.sam", + "true", + null, + null, + false, + null, + "nonexistent.key" + )), + properties( + """ + com.amy=true + com.zoe=true + """, + """ + com.amy=true + com.zoe=true + com.sam=true + """ + ) + ); + } + + @Test + void afterPropertyLastEntry() { + rewriteRun( + spec -> spec.recipe(new AddProperty( + "zzz.last", + "val", + null, + null, + null, + null, + "com.zoe" + )), + properties( + """ + com.amy=true + com.zoe=true + """, + """ + com.amy=true + com.zoe=true + zzz.last=val + """ + ) + ); + } + + @Test + void beforeAndAfterBothSetValidation() { + AddProperty recipe = new AddProperty( + "new.prop", + "val", + null, + null, + null, + "some.key", + "other.key" + ); + assertThat(recipe.validate().isInvalid()).isTrue(); + } + + @Test + void beforePropertyOverridesOrderedInsertion() { + rewriteRun( + spec -> spec.recipe(new AddProperty( + "zzz.prop", + "val", + null, + null, + true, + "com.amy", + null + )), + properties( + """ + com.amy=true + com.zoe=true + """, + """ + zzz.prop=val + com.amy=true + com.zoe=true + """ + ) + ); + } + }