1919import lombok .Value ;
2020import org .jspecify .annotations .Nullable ;
2121import org .openrewrite .*;
22+ import org .openrewrite .internal .ListUtils ;
23+ import org .openrewrite .internal .NameCaseConvention ;
2224import org .openrewrite .internal .StringUtils ;
25+ import org .openrewrite .java .AnnotationMatcher ;
26+ import org .openrewrite .java .JavaIsoVisitor ;
27+ import org .openrewrite .java .search .UsesType ;
28+ import org .openrewrite .java .tree .Expression ;
29+ import org .openrewrite .java .tree .J ;
30+ import org .openrewrite .java .tree .JavaSourceFile ;
31+ import org .openrewrite .kotlin .tree .K ;
2332import org .openrewrite .properties .tree .Properties ;
2433import org .openrewrite .yaml .tree .Yaml ;
2534
35+ import java .util .regex .Matcher ;
2636import java .util .regex .Pattern ;
2737
2838@ EqualsAndHashCode (callSuper = false )
@@ -31,7 +41,8 @@ public class ChangeSpringPropertyValue extends Recipe {
3141
3242 String displayName = "Change the value of a spring application property" ;
3343
34- String description = "Change spring application property values existing in either Properties or Yaml files." ;
44+ String description = "Change Spring application property values existing in either Properties or YAML files, " +
45+ "and in `@Value`, `@ConditionalOnProperty`, `@SpringBootTest`, or `@TestPropertySource` annotations." ;
3546
3647 @ Option (displayName = "Property key" ,
3748 description = "The name of the property key whose value is to be changed." ,
@@ -75,13 +86,21 @@ public TreeVisitor<?, ExecutionContext> getVisitor() {
7586 Recipe changeProperties = new org .openrewrite .properties .ChangePropertyValue (propertyKey , newValue , oldValue , regex , relaxedBinding );
7687 String yamlValue = quoteValue (newValue ) ? "\" " + newValue + "\" " : newValue ;
7788 Recipe changeYaml = new org .openrewrite .yaml .ChangePropertyValue (propertyKey , yamlValue , oldValue , regex , relaxedBinding , null );
89+ TreeVisitor <?, ExecutionContext > javaVisitor = Preconditions .check (Preconditions .or (
90+ new UsesType <>("org.springframework.beans.factory.annotation.Value" , false ),
91+ new UsesType <>("org.springframework.boot.autoconfigure.condition.ConditionalOnProperty" , false ),
92+ new UsesType <>("org.springframework.boot..*Test" , false ),
93+ new UsesType <>("org.springframework.test.context.TestPropertySource" , false )
94+ ), new JavaPropertyValueVisitor ());
7895 return new TreeVisitor <Tree , ExecutionContext >() {
7996 @ Override
8097 public @ Nullable Tree visit (@ Nullable Tree tree , ExecutionContext ctx ) {
8198 if (tree instanceof Properties .File ) {
8299 tree = changeProperties .getVisitor ().visit (tree , ctx );
83100 } else if (tree instanceof Yaml .Documents ) {
84101 tree = changeYaml .getVisitor ().visit (tree , ctx );
102+ } else if (tree instanceof JavaSourceFile ) {
103+ tree = javaVisitor .visit (tree , ctx );
85104 }
86105 return tree ;
87106 }
@@ -92,4 +111,241 @@ public TreeVisitor<?, ExecutionContext> getVisitor() {
92111 private boolean quoteValue (String value ) {
93112 return scalarNeedsAQuote .matcher (value ).matches ();
94113 }
114+
115+ private class JavaPropertyValueVisitor extends JavaIsoVisitor <ExecutionContext > {
116+ private final AnnotationMatcher VALUE_MATCHER =
117+ new AnnotationMatcher ("@org.springframework.beans.factory.annotation.Value" );
118+ private final AnnotationMatcher CONDITIONAL_ON_PROPERTY_MATCHER =
119+ new AnnotationMatcher ("@org.springframework.boot.autoconfigure.condition.ConditionalOnProperty" );
120+ private final AnnotationMatcher SPRING_BOOT_TEST_MATCHER =
121+ new AnnotationMatcher ("@org.springframework.boot..*Test" );
122+ private final AnnotationMatcher TEST_PROPERTY_SOURCE_MATCHER =
123+ new AnnotationMatcher ("@org.springframework.test.context.TestPropertySource" );
124+
125+ // Pattern to match ${key:defaultValue} in @Value annotations
126+ private final Pattern valueAnnotationPattern = Pattern .compile ("\\ $\\ {([^:}]+)(?::([^}]*))?\\ }" );
127+ // Pattern to match key=value in test annotations
128+ private final Pattern keyValuePattern = Pattern .compile ("^([^=]+)=(.*)$" );
129+
130+ @ Override
131+ public J .Annotation visitAnnotation (J .Annotation annotation , ExecutionContext ctx ) {
132+ J .Annotation a = super .visitAnnotation (annotation , ctx );
133+
134+ if (VALUE_MATCHER .matches (a )) {
135+ a = handleValueAnnotation (a );
136+ } else if (CONDITIONAL_ON_PROPERTY_MATCHER .matches (a )) {
137+ a = handleConditionalOnPropertyAnnotation (a );
138+ } else if (SPRING_BOOT_TEST_MATCHER .matches (a ) || TEST_PROPERTY_SOURCE_MATCHER .matches (a )) {
139+ a = handleTestPropertiesAnnotation (a );
140+ }
141+
142+ return a ;
143+ }
144+
145+ private J .Annotation handleValueAnnotation (J .Annotation annotation ) {
146+ return annotation .withArguments (ListUtils .map (annotation .getArguments (), arg -> {
147+ if (arg instanceof J .Literal ) {
148+ return changeValueInValueAnnotation ((J .Literal ) arg );
149+ }
150+ return arg ;
151+ }));
152+ }
153+
154+ private J .Literal changeValueInValueAnnotation (J .Literal literal ) {
155+ if (!(literal .getValue () instanceof String )) {
156+ return literal ;
157+ }
158+ String value = (String ) literal .getValue ();
159+ String valueSource = literal .getValueSource ();
160+ Matcher matcher = valueAnnotationPattern .matcher (value );
161+
162+ boolean changed = false ;
163+ StringBuilder newValue = new StringBuilder ();
164+ int lastEnd = 0 ;
165+
166+ while (matcher .find ()) {
167+ String key = matcher .group (1 );
168+ String defaultValue = matcher .group (2 );
169+
170+ if (matchesPropertyKey (key ) && defaultValue != null && matchesOldValue (defaultValue )) {
171+ String computedNewValue = computeNewValue (defaultValue );
172+ if (!computedNewValue .equals (defaultValue )) {
173+ newValue .append (value , lastEnd , matcher .start ());
174+ newValue .append ("${" ).append (key ).append (":" ).append (computedNewValue ).append ("}" );
175+ lastEnd = matcher .end ();
176+
177+ // Also update valueSource by replacing the old default with new default
178+ // This preserves all other escaping (e.g., \$ in Kotlin)
179+ if (valueSource != null ) {
180+ valueSource = valueSource .replace (defaultValue , computedNewValue );
181+ }
182+
183+ changed = true ;
184+ }
185+ }
186+ }
187+
188+ if (changed ) {
189+ newValue .append (value , lastEnd , value .length ());
190+ String newValueStr = newValue .toString ();
191+ if (valueSource == null ) {
192+ valueSource = "\" " + newValueStr + "\" " ;
193+ }
194+ return literal .withValue (newValueStr ).withValueSource (valueSource );
195+ }
196+ return literal ;
197+ }
198+
199+ private J .Annotation handleConditionalOnPropertyAnnotation (J .Annotation annotation ) {
200+ if (annotation .getArguments () == null ) {
201+ return annotation ;
202+ }
203+
204+ // First, find the property key from 'name' or 'value' attribute
205+ String foundKey = null ;
206+ for (Expression arg : annotation .getArguments ()) {
207+ if (arg instanceof J .Assignment ) {
208+ J .Assignment assignment = (J .Assignment ) arg ;
209+ String attrName = ((J .Identifier ) assignment .getVariable ()).getSimpleName ();
210+ if ("name" .equals (attrName ) || "value" .equals (attrName )) {
211+ if (assignment .getAssignment () instanceof J .Literal ) {
212+ Object val = ((J .Literal ) assignment .getAssignment ()).getValue ();
213+ if (val instanceof String ) {
214+ foundKey = (String ) val ;
215+ break ;
216+ }
217+ }
218+ }
219+ } else if (arg instanceof J .Literal && ((J .Literal ) arg ).getValue () instanceof String ) {
220+ // First unnamed argument is the property name
221+ foundKey = (String ) ((J .Literal ) arg ).getValue ();
222+ break ;
223+ }
224+ }
225+
226+ if (foundKey == null || !matchesPropertyKey (foundKey )) {
227+ return annotation ;
228+ }
229+
230+ // Now change the 'havingValue' attribute
231+ return annotation .withArguments (ListUtils .map (annotation .getArguments (), arg -> {
232+ if (arg instanceof J .Assignment ) {
233+ J .Assignment assignment = (J .Assignment ) arg ;
234+ String attrName = ((J .Identifier ) assignment .getVariable ()).getSimpleName ();
235+ if ("havingValue" .equals (attrName )) {
236+ if (assignment .getAssignment () instanceof J .Literal ) {
237+ J .Literal literal = (J .Literal ) assignment .getAssignment ();
238+ J .Literal newLiteral = changeValueInLiteral (literal );
239+ if (newLiteral != literal ) {
240+ return assignment .withAssignment (newLiteral );
241+ }
242+ }
243+ }
244+ }
245+ return arg ;
246+ }));
247+ }
248+
249+ private J .Literal changeValueInLiteral (J .Literal literal ) {
250+ String value = (String ) literal .getValue ();
251+ if (matchesOldValue (value )) {
252+ String computedNewValue = computeNewValue (value );
253+ if (!computedNewValue .equals (value )) {
254+ return updateLiteral (literal , computedNewValue );
255+ }
256+ }
257+ return literal ;
258+ }
259+
260+ private J .Annotation handleTestPropertiesAnnotation (J .Annotation annotation ) {
261+ return annotation .withArguments (ListUtils .map (annotation .getArguments (), arg -> {
262+ if (arg instanceof J .Assignment ) {
263+ J .Assignment assignment = (J .Assignment ) arg ;
264+ String attrName = ((J .Identifier ) assignment .getVariable ()).getSimpleName ();
265+ if ("properties" .equals (attrName )) {
266+ if (assignment .getAssignment () instanceof J .Literal ) {
267+ J .Literal literal = (J .Literal ) assignment .getAssignment ();
268+ J .Literal newLiteral = changeValueInTestProperty (literal );
269+ return assignment .withAssignment (newLiteral );
270+ }
271+ if (assignment .getAssignment () instanceof J .NewArray ) {
272+ J .NewArray array = (J .NewArray ) assignment .getAssignment ();
273+ return assignment .withAssignment (array .withInitializer (ListUtils .map (array .getInitializer (),
274+ element -> element instanceof J .Literal ? changeValueInTestProperty ((J .Literal ) element ) : element )));
275+ }
276+ if (assignment .getAssignment () instanceof K .ListLiteral ) {
277+ K .ListLiteral listLiteral = (K .ListLiteral ) assignment .getAssignment ();
278+ return assignment .withAssignment (listLiteral .withElements (ListUtils .map (listLiteral .getElements (),
279+ element -> element instanceof J .Literal ? changeValueInTestProperty ((J .Literal ) element ) : element )));
280+ }
281+ }
282+ }
283+ return arg ;
284+ }));
285+ }
286+
287+ private J .Literal changeValueInTestProperty (J .Literal literal ) {
288+ String value = (String ) literal .getValue ();
289+ Matcher matcher = keyValuePattern .matcher (value );
290+ if (matcher .matches ()) {
291+ String key = matcher .group (1 );
292+ String propValue = matcher .group (2 );
293+ if (matchesPropertyKey (key ) && matchesOldValue (propValue )) {
294+ String computedNewValue = computeNewValue (propValue );
295+ if (!computedNewValue .equals (propValue )) {
296+ return updateLiteral (literal , key + "=" + computedNewValue );
297+ }
298+ }
299+ }
300+ return literal ;
301+ }
302+
303+ private boolean matchesPropertyKey (String key ) {
304+ if (!Boolean .FALSE .equals (relaxedBinding )) {
305+ // Normalize dots to hyphens for relaxed binding comparison
306+ // (NameCaseConvention doesn't handle dots as separators)
307+ String normalizedKey = key .replace ('.' , '-' );
308+ String normalizedPropertyKey = propertyKey .replace ('.' , '-' );
309+ return NameCaseConvention .equalsRelaxedBinding (normalizedKey , normalizedPropertyKey );
310+ }
311+ return key .equals (propertyKey );
312+ }
313+
314+ private boolean matchesOldValue (String value ) {
315+ // Don't match if the value is already the target value (idempotency)
316+ if (value .equals (newValue )) {
317+ return false ;
318+ }
319+ if (oldValue == null ) {
320+ return true ;
321+ }
322+ if (Boolean .TRUE .equals (regex )) {
323+ return Pattern .compile (oldValue ).matcher (value ).find ();
324+ }
325+ return value .equals (oldValue );
326+ }
327+
328+ private String computeNewValue (String currentValue ) {
329+ if (Boolean .TRUE .equals (regex ) && oldValue != null ) {
330+ String computed = Pattern .compile (oldValue ).matcher (currentValue ).replaceFirst (newValue );
331+ // Return original if no change to ensure idempotency
332+ return computed .equals (currentValue ) ? currentValue : computed ;
333+ }
334+ return newValue ;
335+ }
336+
337+ private J .Literal updateLiteral (J .Literal literal , String newValueStr ) {
338+ String valueSource = literal .getValueSource ();
339+ if (valueSource == null ) {
340+ return literal .withValue (newValueStr ).withValueSource ("\" " + newValueStr + "\" " );
341+ }
342+ // Handle escaping for the valueSource
343+ char quote = valueSource .charAt (0 );
344+ String escapedValue = newValueStr .replace ("\\ " , "\\ \\ " );
345+ if (quote == '"' ) {
346+ escapedValue = escapedValue .replace ("\" " , "\\ \" " );
347+ }
348+ return literal .withValue (newValueStr ).withValueSource (quote + escapedValue + quote );
349+ }
350+ }
95351}
0 commit comments