Skip to content

Commit e94d36f

Browse files
Optimize ChangePropertyKey (#6622)
* Optimize `ChangePropertyKey` Avoid many repeated `Pattern` allocations. * Polish * Polish
1 parent 16f878d commit e94d36f

7 files changed

Lines changed: 178 additions & 71 deletions

File tree

rewrite-core/src/main/java/org/openrewrite/internal/NameCaseConvention.java

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.openrewrite.internal;
1717

18+
import org.jspecify.annotations.Nullable;
1819
import org.openrewrite.Incubating;
1920

2021
import java.util.regex.Pattern;
@@ -53,7 +54,14 @@ public enum NameCaseConvention {
5354
* This is the standard Java and C++ constant variable naming convention.
5455
* This is also known as "SCREAMING_SNAKE_CASE".
5556
*/
56-
UPPER_UNDERSCORE;
57+
UPPER_UNDERSCORE,
58+
59+
/**
60+
* No transformation - returns the input string unchanged.
61+
* Use this for exact matching without any case normalization.
62+
*/
63+
@Incubating(since = "8.73.0")
64+
EXACT;
5765

5866
private static final Pattern CAMEL_CASE_SPLIT = Pattern.compile("[\\s_-]");
5967
private static final int uppercaseAbbreviationMinLength = 3;
@@ -191,6 +199,80 @@ public static boolean matchesRegexRelaxedBinding(String test, String pattern) {
191199
return LOWER_CAMEL.format(test).matches(LOWER_CAMEL.format(pattern));
192200
}
193201

202+
/**
203+
* Compiles a pattern for efficient repeated matching against this convention.
204+
* The pattern is formatted once during compilation, avoiding repeated conversions
205+
* when matching multiple test strings.
206+
*
207+
* @param pattern The pattern to compile
208+
* @return A compiled pattern that can be used for efficient matching
209+
* @see #matchesGlobRelaxedBinding(String, String)
210+
*/
211+
@Incubating(since = "8.73.0")
212+
public Compiled compile(String pattern) {
213+
return new Compiled(this, pattern);
214+
}
215+
216+
/**
217+
* A compiled pattern for efficient repeated matching.
218+
* <p>
219+
* This is useful when matching multiple test strings against a constant pattern.
220+
* The pattern is converted to the target convention once during construction,
221+
* avoiding repeated conversions when calling {@link #matchesGlob(String)} or {@link #matchesRegex(String)}.
222+
*
223+
* @see NameCaseConvention#compile(String)
224+
*/
225+
@Incubating(since = "8.73.0")
226+
public static class Compiled {
227+
private final NameCaseConvention convention;
228+
private final String formattedPattern;
229+
private volatile @Nullable Pattern compiledRegex;
230+
231+
private Compiled(NameCaseConvention convention, String pattern) {
232+
this.convention = convention;
233+
this.formattedPattern = convention.format(pattern);
234+
}
235+
236+
/**
237+
* Tests whether the given string equals the compiled pattern after formatting.
238+
*
239+
* @param test The string to test
240+
* @return {@code true} if the formatted test string equals the formatted pattern
241+
*/
242+
public boolean matches(String test) {
243+
return convention.format(test).equals(formattedPattern);
244+
}
245+
246+
/**
247+
* Tests whether the given string matches the compiled pattern using glob matching.
248+
*
249+
* @param test The string to test
250+
* @return {@code true} if the test string matches the pattern
251+
*/
252+
public boolean matchesGlob(String test) {
253+
return StringUtils.matchesGlob(convention.format(test), formattedPattern);
254+
}
255+
256+
/**
257+
* Tests whether the given string matches the compiled pattern using regex matching.
258+
*
259+
* @param test The string to test
260+
* @return {@code true} if the test string matches the pattern
261+
*/
262+
public boolean matchesRegex(String test) {
263+
Pattern p = compiledRegex;
264+
if (p == null) {
265+
synchronized (this) {
266+
p = compiledRegex;
267+
if (p == null) {
268+
compiledRegex = p = Pattern.compile(formattedPattern);
269+
}
270+
}
271+
}
272+
return p.matcher(convention.format(test)).matches();
273+
}
274+
}
275+
194276
private static String lowerHyphen(String str) {
195277
return nameCaseJoiner(str.replace('_', '-').replace(' ', '-'), true, '-');
196278
}

rewrite-properties/src/main/java/org/openrewrite/properties/ChangePropertyKey.java

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -58,31 +58,26 @@ public class ChangePropertyKey extends Recipe {
5858

5959
@Override
6060
public TreeVisitor<?, ExecutionContext> getVisitor() {
61-
return new ChangePropertyKeyVisitor<>();
62-
}
63-
64-
public class ChangePropertyKeyVisitor<P> extends PropertiesVisitor<P> {
65-
public ChangePropertyKeyVisitor() {
66-
}
61+
NameCaseConvention.Compiled keyMatcher = (!Boolean.FALSE.equals(relaxedBinding) ?
62+
NameCaseConvention.LOWER_CAMEL :
63+
NameCaseConvention.EXACT).compile(oldPropertyKey);
6764

68-
@Override
69-
public Properties visitEntry(Properties.Entry entry, P p) {
70-
if (Boolean.TRUE.equals(regex)) {
71-
if (!Boolean.FALSE.equals(relaxedBinding) ?
72-
NameCaseConvention.matchesRegexRelaxedBinding(entry.getKey(), oldPropertyKey) :
73-
entry.getKey().matches(oldPropertyKey)) {
74-
entry = entry.withKey(entry.getKey().replaceFirst(oldPropertyKey, newPropertyKey))
75-
.withPrefix(entry.getPrefix());
76-
}
77-
} else {
78-
if (!Boolean.FALSE.equals(relaxedBinding) ?
79-
NameCaseConvention.equalsRelaxedBinding(entry.getKey(), oldPropertyKey) :
80-
entry.getKey().equals(oldPropertyKey)) {
81-
entry = entry.withKey(newPropertyKey)
82-
.withPrefix(entry.getPrefix());
65+
return new PropertiesVisitor<ExecutionContext>() {
66+
@Override
67+
public Properties visitEntry(Properties.Entry entry, ExecutionContext ctx) {
68+
if (Boolean.TRUE.equals(regex)) {
69+
if (keyMatcher.matchesRegex(entry.getKey())) {
70+
entry = entry.withKey(entry.getKey().replaceFirst(oldPropertyKey, newPropertyKey))
71+
.withPrefix(entry.getPrefix());
72+
}
73+
} else {
74+
if (keyMatcher.matches(entry.getKey())) {
75+
entry = entry.withKey(newPropertyKey)
76+
.withPrefix(entry.getPrefix());
77+
}
8378
}
79+
return super.visitEntry(entry, ctx);
8480
}
85-
return super.visitEntry(entry, p);
86-
}
81+
};
8782
}
8883
}

rewrite-properties/src/main/java/org/openrewrite/properties/ChangePropertyValue.java

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,17 +71,26 @@ public Validated validate() {
7171

7272
@Override
7373
public TreeVisitor<?, ExecutionContext> getVisitor() {
74-
return new ChangePropertyValueVisitor<>();
74+
return new ChangePropertyValueVisitor<>((!Boolean.FALSE.equals(relaxedBinding) ?
75+
NameCaseConvention.LOWER_CAMEL :
76+
NameCaseConvention.EXACT).compile(propertyKey));
7577
}
7678

7779
public class ChangePropertyValueVisitor<P> extends PropertiesVisitor<P> {
80+
private final NameCaseConvention.Compiled keyMatcher;
81+
7882
public ChangePropertyValueVisitor() {
83+
this(NameCaseConvention.EXACT.compile(propertyKey));
84+
}
85+
86+
public ChangePropertyValueVisitor(NameCaseConvention.Compiled keyMatcher) {
87+
this.keyMatcher = keyMatcher;
7988
}
8089

8190
@Override
8291
public Properties visitEntry(Properties.Entry entry, P p) {
8392
Properties.Entry e = (Properties.Entry) super.visitEntry(entry, p);
84-
if (matchesPropertyKey(e.getKey()) && matchesOldValue(e.getValue())) {
93+
if (keyMatcher.matchesGlob(e.getKey()) && matchesOldValue(e.getValue())) {
8594
Properties.Value updatedValue = updateValue(e.getValue());
8695
if (updatedValue != null) {
8796
e = e.withValue(updatedValue);
@@ -98,12 +107,6 @@ public Properties visitEntry(Properties.Entry entry, P p) {
98107
return updatedValue.getText().equals(value.getText()) ? null : updatedValue;
99108
}
100109

101-
private boolean matchesPropertyKey(String prop) {
102-
return !Boolean.FALSE.equals(relaxedBinding) ?
103-
NameCaseConvention.matchesGlobRelaxedBinding(prop, propertyKey) :
104-
StringUtils.matchesGlob(prop, propertyKey);
105-
}
106-
107110
private boolean matchesOldValue(Properties.Value value) {
108111
return StringUtils.isNullOrEmpty(oldValue) ||
109112
(Boolean.TRUE.equals(regex) ?

rewrite-properties/src/main/java/org/openrewrite/properties/search/FindProperties.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,14 @@ public class FindProperties extends Recipe {
6161
* @return The set of found properties matching the propertyKey.
6262
*/
6363
public static Set<Properties.Entry> find(Properties p, String propertyKey, @Nullable Boolean relaxedBinding) {
64+
NameCaseConvention.Compiled keyMatcher = (!Boolean.FALSE.equals(relaxedBinding) ?
65+
NameCaseConvention.LOWER_CAMEL :
66+
NameCaseConvention.EXACT).compile(propertyKey);
67+
6468
PropertiesVisitor<Set<Properties.Entry>> findVisitor = new PropertiesVisitor<Set<Properties.Entry>>() {
6569
@Override
6670
public Properties visitEntry(Properties.Entry entry, Set<Properties.Entry> ps) {
67-
if (!Boolean.FALSE.equals(relaxedBinding) ? NameCaseConvention.matchesGlobRelaxedBinding(entry.getKey(), propertyKey) :
68-
StringUtils.matchesGlob(entry.getKey(), propertyKey)) {
71+
if (keyMatcher.matchesGlob(entry.getKey())) {
6972
ps.add(entry);
7073
}
7174
return super.visitEntry(entry, ps);
@@ -79,11 +82,14 @@ public Properties visitEntry(Properties.Entry entry, Set<Properties.Entry> ps) {
7982

8083
@Override
8184
public TreeVisitor<?, ExecutionContext> getVisitor() {
85+
NameCaseConvention.Compiled keyMatcher = (!Boolean.FALSE.equals(relaxedBinding) ?
86+
NameCaseConvention.LOWER_CAMEL :
87+
NameCaseConvention.EXACT).compile(propertyKey);
88+
8289
return new PropertiesVisitor<ExecutionContext>() {
8390
@Override
8491
public Properties visitEntry(Properties.Entry entry, ExecutionContext ctx) {
85-
if (!Boolean.FALSE.equals(relaxedBinding) ? NameCaseConvention.matchesGlobRelaxedBinding(entry.getKey(), propertyKey) :
86-
StringUtils.matchesGlob(entry.getKey(), propertyKey)) {
92+
if (keyMatcher.matchesGlob(entry.getKey())) {
8793
entry = entry.withValue(entry.getValue().withMarkers(entry.getValue().getMarkers()
8894
.computeByType(new SearchResult(randomId(), null), (s1, s2) -> s1 == null ? s2 : s1)));
8995
}

rewrite-yaml/src/main/java/org/openrewrite/yaml/ChangePropertyKey.java

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import org.openrewrite.*;
2222
import org.openrewrite.internal.ListUtils;
2323
import org.openrewrite.internal.NameCaseConvention;
24-
import org.openrewrite.internal.StringUtils;
2524
import org.openrewrite.marker.Markers;
2625
import org.openrewrite.yaml.search.FindProperty;
2726
import org.openrewrite.yaml.tree.Yaml;
@@ -92,6 +91,30 @@ public TreeVisitor<?, ExecutionContext> getVisitor() {
9291
}
9392

9493
private class ChangePropertyKeyVisitor<P> extends YamlIsoVisitor<P> {
94+
private final NameCaseConvention.Compiled oldKeyMatcher;
95+
private final NameCaseConvention.Compiled oldKeyWildcardMatcher;
96+
private final NameCaseConvention.Compiled newKeyMatcher;
97+
private final NameCaseConvention.Compiled newKeyWildcardMatcher;
98+
private final List<NameCaseConvention.Compiled> exceptMatchers;
99+
private final List<NameCaseConvention.Compiled> exceptWildcardMatchers;
100+
101+
ChangePropertyKeyVisitor() {
102+
NameCaseConvention convention = !Boolean.FALSE.equals(relaxedBinding) ?
103+
NameCaseConvention.LOWER_CAMEL :
104+
NameCaseConvention.EXACT;
105+
this.oldKeyMatcher = convention.compile(oldPropertyKey);
106+
this.oldKeyWildcardMatcher = convention.compile(oldPropertyKey + ".*");
107+
this.newKeyMatcher = convention.compile(newPropertyKey);
108+
this.newKeyWildcardMatcher = convention.compile(newPropertyKey + ".*");
109+
List<String> excluded = excludedSubKeys();
110+
this.exceptMatchers = new ArrayList<>(excluded.size());
111+
this.exceptWildcardMatchers = new ArrayList<>(excluded.size());
112+
for (String subkey : excluded) {
113+
exceptMatchers.add(convention.compile(oldPropertyKey + "." + subkey));
114+
exceptWildcardMatchers.add(convention.compile(oldPropertyKey + "." + subkey + ".*"));
115+
}
116+
}
117+
95118
@Override
96119
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, P p) {
97120
Yaml.Mapping.Entry e = super.visitMappingEntry(entry, p);
@@ -108,12 +131,14 @@ public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, P p) {
108131
.collect(joining("."));
109132

110133
if (newPropertyKey.startsWith(oldPropertyKey) &&
111-
(matches(prop, newPropertyKey) || matches(prop, newPropertyKey + ".*") || childMatchesNewPropertyKey(entry, prop))) {
134+
(newKeyMatcher.matchesGlob(prop) ||
135+
newKeyWildcardMatcher.matchesGlob(prop) ||
136+
childMatchesNewPropertyKey(entry, prop))) {
112137
return e;
113138
}
114139

115140
String propertyToTest = newPropertyKey;
116-
if (matches(prop, oldPropertyKey)) {
141+
if (oldKeyMatcher.matchesGlob(prop)) {
117142
Iterator<Yaml.Mapping.Entry> propertyEntriesLeftToRight = propertyEntries.descendingIterator();
118143
while (propertyEntriesLeftToRight.hasNext()) {
119144
Yaml.Mapping.Entry propertyEntry = propertyEntriesLeftToRight.next();
@@ -132,9 +157,10 @@ public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, P p) {
132157
}
133158
} else {
134159
String parentProp = prop.substring(0, prop.length() - e.getKey().getValue().length()).replaceAll(".$", "");
135-
if (matches(prop, oldPropertyKey + ".*") &&
136-
!(matches(parentProp, oldPropertyKey + ".*") || matches(parentProp, oldPropertyKey)) &&
137-
noneMatch(prop, oldPropertyKey, excludedSubKeys())) {
160+
if (oldKeyWildcardMatcher.matchesGlob(prop) &&
161+
!(oldKeyWildcardMatcher.matchesGlob(parentProp) ||
162+
oldKeyMatcher.matchesGlob(parentProp)) &&
163+
noneMatchExcluded(prop)) {
138164
Iterator<Yaml.Mapping.Entry> propertyEntriesLeftToRight = propertyEntries.descendingIterator();
139165
while (propertyEntriesLeftToRight.hasNext()) {
140166
Yaml.Mapping.Entry propertyEntry = propertyEntriesLeftToRight.next();
@@ -162,6 +188,15 @@ private boolean childMatchesNewPropertyKey(Yaml.Mapping.Entry entry, String curs
162188
entry.getKey().getValue());
163189
return !FindProperty.find(entry, rescopedNewPropertyKey, relaxedBinding).isEmpty();
164190
}
191+
192+
private boolean noneMatchExcluded(String key) {
193+
for (int i = 0; i < exceptMatchers.size(); i++) {
194+
if (exceptMatchers.get(i).matchesGlob(key) || exceptWildcardMatchers.get(i).matchesGlob(key)) {
195+
return false;
196+
}
197+
}
198+
return true;
199+
}
165200
}
166201

167202
private boolean hasNonExcludedValues(Yaml.Mapping.Entry propertyEntry) {
@@ -208,22 +243,6 @@ private static boolean noneMatch(Yaml.Mapping.Entry entry, List<String> subKeys)
208243
return true;
209244
}
210245

211-
private boolean noneMatch(String key, String basePattern, List<String> excludedSubKeys) {
212-
for (String subkey : excludedSubKeys) {
213-
String subKeyPattern = basePattern + "." + subkey;
214-
if (matches(key, subKeyPattern) || matches(key, subKeyPattern + ".*")) {
215-
return false;
216-
}
217-
}
218-
return true;
219-
}
220-
221-
private boolean matches(String string, String pattern) {
222-
return !Boolean.FALSE.equals(relaxedBinding) ?
223-
NameCaseConvention.matchesGlobRelaxedBinding(string, pattern) :
224-
StringUtils.matchesGlob(string, pattern);
225-
}
226-
227246
private List<String> excludedSubKeys() {
228247
return except != null ? except : emptyList();
229248
}

rewrite-yaml/src/main/java/org/openrewrite/yaml/ChangePropertyValue.java

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,16 @@ public Validated<Object> validate() {
8686

8787
@Override
8888
public TreeVisitor<?, ExecutionContext> getVisitor() {
89+
NameCaseConvention.Compiled keyMatcher = (!Boolean.FALSE.equals(relaxedBinding) ?
90+
NameCaseConvention.LOWER_CAMEL :
91+
NameCaseConvention.EXACT).compile(propertyKey);
92+
8993
return Preconditions.check(new FindSourceFiles(filePattern), new YamlIsoVisitor<ExecutionContext>() {
9094
@Override
9195
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {
9296
Yaml.Mapping.Entry e = super.visitMappingEntry(entry, ctx);
9397
String prop = getProperty(getCursor());
94-
if (matchesPropertyKey(prop) && matchesOldValue(e.getValue())) {
98+
if (keyMatcher.matchesGlob(prop) && matchesOldValue(e.getValue())) {
9599
Yaml.Block updatedValue = updateValue(e.getValue());
96100
if (updatedValue != null) {
97101
e = e.withValue(updatedValue);
@@ -126,12 +130,6 @@ public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionC
126130
return null;
127131
}
128132

129-
private boolean matchesPropertyKey(String prop) {
130-
return !Boolean.FALSE.equals(relaxedBinding) ?
131-
NameCaseConvention.matchesGlobRelaxedBinding(prop, propertyKey) :
132-
StringUtils.matchesGlob(prop, propertyKey);
133-
}
134-
135133
private boolean matchesOldValue(Yaml.Block value) {
136134
if (value instanceof Yaml.Scalar) {
137135
Yaml.Scalar scalar = (Yaml.Scalar) value;

0 commit comments

Comments
 (0)