Skip to content

Commit cdf1a7a

Browse files
authored
More marketplace improvements (#6326)
* MavenRecipeBundleResolver sets the version on the bundle supplied to MavenRecipeBundleReader * Only read recipes defined in a Maven bundle but not its dependencies * Fix install
1 parent e268a63 commit cdf1a7a

8 files changed

Lines changed: 117 additions & 73 deletions

File tree

rewrite-core/src/main/java/org/openrewrite/Recipe.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,16 @@
1515
*/
1616
package org.openrewrite;
1717

18+
import com.fasterxml.jackson.annotation.JsonAutoDetect;
1819
import com.fasterxml.jackson.annotation.JsonProperty;
1920
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
2021
import com.fasterxml.jackson.annotation.JsonTypeInfo;
22+
import com.fasterxml.jackson.databind.DeserializationFeature;
23+
import com.fasterxml.jackson.databind.MapperFeature;
24+
import com.fasterxml.jackson.databind.ObjectMapper;
25+
import com.fasterxml.jackson.databind.cfg.ConstructorDetector;
26+
import com.fasterxml.jackson.databind.json.JsonMapper;
27+
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
2128
import lombok.AccessLevel;
2229
import lombok.RequiredArgsConstructor;
2330
import lombok.Setter;
@@ -33,6 +40,8 @@
3340
import org.openrewrite.table.SourcesFileErrors;
3441
import org.openrewrite.table.SourcesFileResults;
3542

43+
import java.io.IOException;
44+
import java.io.UncheckedIOException;
3645
import java.lang.reflect.Constructor;
3746
import java.lang.reflect.Field;
3847
import java.lang.reflect.Method;
@@ -529,6 +538,40 @@ public static Builder builder(@NlsRewrite.DisplayName @Language("markdown") Stri
529538
return new Builder(displayName, description);
530539
}
531540

541+
@Incubating(since = "8.67.0")
542+
public Recipe withOptions(@Nullable Map<String, Object> options) {
543+
Map<String, Object> m = new HashMap<>();
544+
m.put("@c", getName());
545+
ObjectMapper objectMapper = JsonMapper.builder()
546+
.constructorDetector(ConstructorDetector.USE_PROPERTIES_BASED)
547+
.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES)
548+
.configure(MapperFeature.PROPAGATE_TRANSIENT_MARKER, true)
549+
.build()
550+
.registerModule(new ParameterNamesModule())
551+
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
552+
// This is necessary to allow setting options like `FindTags#xPath`, as Jackson otherwise only sees a `xpath`
553+
// property, which it derives from the `getXPath()` method generated by Lombok
554+
objectMapper.setVisibility(objectMapper.getSerializationConfig().getDefaultVisibilityChecker()
555+
.withFieldVisibility(JsonAutoDetect.Visibility.ANY));
556+
try {
557+
Recipe clone = clone();
558+
if (options != null) {
559+
m.putAll(options);
560+
for (OptionDescriptor optionDescriptor : clone.getDescriptor().getOptions()) {
561+
Object value = options.get(optionDescriptor.getName());
562+
if (value instanceof String) {
563+
Map<String, Object> option = new HashMap<>();
564+
option.put("value", value);
565+
objectMapper.updateValue(optionDescriptor, option);
566+
}
567+
}
568+
}
569+
return objectMapper.updateValue(clone, m);
570+
} catch (IOException e) {
571+
throw new UncheckedIOException(e);
572+
}
573+
}
574+
532575
@Incubating(since = "8.31.0")
533576
@RequiredArgsConstructor
534577
@FieldDefaults(level = AccessLevel.PRIVATE)

rewrite-core/src/main/java/org/openrewrite/marketplace/RecipeBundleReader.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,6 @@ public interface RecipeBundleReader {
2828
RecipeDescriptor describe(RecipeListing listing);
2929

3030
Recipe prepare(RecipeListing listing, Map<String, Object> options);
31+
32+
ClassLoader classLoader();
3133
}

rewrite-core/src/main/java/org/openrewrite/marketplace/RecipeListing.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
import java.time.Duration;
2525
import java.util.List;
2626
import java.util.Map;
27-
import java.util.stream.Collectors;
27+
28+
import static java.util.stream.Collectors.toList;
2829

2930
@Getter
3031
@RequiredArgsConstructor
@@ -33,7 +34,7 @@ public class RecipeListing implements Comparable<RecipeListing> {
3334
/**
3435
* The marketplace that this listing belongs to.
3536
*/
36-
@With(AccessLevel.PACKAGE)
37+
@With
3738
private final @Nullable RecipeMarketplace marketplace;
3839

3940
private final @EqualsAndHashCode.Include String name;
@@ -64,6 +65,10 @@ public Recipe prepare(Map<String, Object> options) {
6465
return resolve().prepare(this, options);
6566
}
6667

68+
public ClassLoader classLoader() {
69+
return resolve().classLoader();
70+
}
71+
6772
@Override
6873
public int compareTo(RecipeListing o) {
6974
return name.compareTo(o.name);
@@ -91,7 +96,7 @@ public static RecipeListing fromDescriptor(RecipeDescriptor descriptor, RecipeBu
9196
opt.getName(),
9297
opt.getDisplayName(),
9398
opt.getDescription()
94-
)).collect(Collectors.toList()),
99+
)).collect(toList()),
95100
bundle
96101
);
97102
}

rewrite-core/src/main/java/org/openrewrite/marketplace/RecipeMarketplace.java

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,23 @@
1515
*/
1616
package org.openrewrite.marketplace;
1717

18-
import lombok.AccessLevel;
1918
import lombok.Getter;
2019
import lombok.RequiredArgsConstructor;
2120
import lombok.Setter;
2221
import org.jspecify.annotations.Nullable;
2322
import org.openrewrite.Incubating;
2423
import org.openrewrite.NlsRewrite;
2524

26-
import java.util.*;
25+
import java.util.ArrayList;
26+
import java.util.List;
27+
import java.util.Set;
28+
import java.util.TreeSet;
2729

2830
import static java.util.Collections.addAll;
2931

3032
@Incubating(since = "8.66.0")
3133
public class RecipeMarketplace {
32-
private final @Getter(AccessLevel.PACKAGE) Category root = new Category("Root");
34+
private final @Getter Category root = new Category("Root");
3335
private final @Getter List<RecipeBundleResolver> resolvers = new ArrayList<>();
3436

3537
public RecipeMarketplace setResolvers(RecipeBundleResolver... resolvers) {
@@ -56,11 +58,16 @@ public void install(RecipeListing recipe, List<String> categoryPath) {
5658

5759
public Set<RecipeListing> install(RecipeBundleReader bundleReader) {
5860
RecipeMarketplace marketplace = bundleReader.read();
59-
getRoot().merge(marketplace.getRoot());
60-
getRoot().updateBundle(marketplace.getAllRecipes(), bundleReader.getBundle());
61+
RecipeBundle bundle = bundleReader.getBundle();
62+
uninstall(bundle.getPackageEcosystem(), bundle.getPackageName());
63+
root.merge(marketplace.getRoot());
6164
return marketplace.getAllRecipes();
6265
}
6366

67+
public void uninstall(String packageEcosystem, String packageName) {
68+
root.uninstall(packageEcosystem, packageName);
69+
}
70+
6471
@Getter
6572
@RequiredArgsConstructor
6673
public class Category {
@@ -95,15 +102,11 @@ public void merge(Category category) {
95102
}
96103
}
97104

98-
void updateBundle(Collection<RecipeListing> recipes, RecipeBundle bundle) {
99-
for (RecipeListing recipe : recipes) {
100-
if (this.recipes.contains(recipe)) {
101-
this.recipes.remove(recipe);
102-
this.recipes.add(recipe.withBundle(bundle));
103-
}
104-
}
105+
public void uninstall(String packageEcosystem, String packageName) {
106+
recipes.removeIf(r -> r.getBundle().getPackageName().equals(packageName) &&
107+
r.getBundle().getPackageEcosystem().equals(packageEcosystem));
105108
for (Category category : categories) {
106-
category.updateBundle(recipes, bundle);
109+
category.uninstall(packageEcosystem, packageName);
107110
}
108111
}
109112

rewrite-core/src/main/java/org/openrewrite/marketplace/RecipeMarketplaceContentValidator.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,7 @@ public class RecipeMarketplaceContentValidator {
3737
public Validated<RecipeMarketplace> validate(RecipeMarketplace marketplace) {
3838
Validated<RecipeMarketplace> validation = Validated.none();
3939
List<String> categoryPath = new ArrayList<>();
40-
validation = validate(marketplace.getRoot(), validation, categoryPath);
41-
return validation;
40+
return validate(marketplace.getRoot(), validation, categoryPath);
4241
}
4342

4443
private Validated<RecipeMarketplace> validate(RecipeMarketplace.Category category,

rewrite-core/src/main/java/org/openrewrite/marketplace/RecipeMarketplaceReader.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ public RecipeMarketplace fromCsv(Reader csv) {
7979
settings.setHeaderExtractionEnabled(false);
8080
settings.setNullValue("");
8181
settings.setDelimiterDetectionEnabled(true, ',', '\t', ';');
82+
// Allow larger content in columns (e.g., long recipe descriptions)
83+
settings.setMaxCharsPerColumn(-1); // No limit
8284

8385
CsvParser parser = new CsvParser(settings);
8486
parser.beginParsing(csv);

rewrite-core/src/main/java/org/openrewrite/marketplace/ThrowingRecipeBundleReader.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,9 @@ public RecipeDescriptor describe(RecipeListing listing) {
4141
public Recipe prepare(RecipeListing listing, Map<String, Object> options) {
4242
throw t;
4343
}
44+
45+
@Override
46+
public ClassLoader classLoader() {
47+
throw t;
48+
}
4449
}

rewrite-maven/src/main/java/org/openrewrite/maven/marketplace/MavenRecipeBundleReader.java

Lines changed: 40 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -15,37 +15,32 @@
1515
*/
1616
package org.openrewrite.maven.marketplace;
1717

18-
import com.fasterxml.jackson.annotation.JsonAutoDetect;
19-
import com.fasterxml.jackson.databind.DeserializationFeature;
20-
import com.fasterxml.jackson.databind.MapperFeature;
21-
import com.fasterxml.jackson.databind.ObjectMapper;
22-
import com.fasterxml.jackson.databind.cfg.ConstructorDetector;
23-
import com.fasterxml.jackson.databind.json.JsonMapper;
24-
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
2518
import lombok.Getter;
2619
import lombok.RequiredArgsConstructor;
2720
import org.jspecify.annotations.Nullable;
2821
import org.openrewrite.Recipe;
2922
import org.openrewrite.config.ClasspathScanningLoader;
3023
import org.openrewrite.config.Environment;
31-
import org.openrewrite.config.OptionDescriptor;
3224
import org.openrewrite.config.RecipeDescriptor;
3325
import org.openrewrite.marketplace.*;
3426
import org.openrewrite.maven.tree.*;
3527
import org.openrewrite.maven.utilities.MavenArtifactDownloader;
3628

3729
import java.io.IOException;
3830
import java.io.InputStream;
39-
import java.io.UncheckedIOException;
4031
import java.nio.file.Path;
41-
import java.util.*;
32+
import java.util.ArrayList;
33+
import java.util.List;
34+
import java.util.Map;
35+
import java.util.Properties;
4236
import java.util.concurrent.ConcurrentHashMap;
4337
import java.util.concurrent.locks.Lock;
4438
import java.util.concurrent.locks.ReentrantLock;
4539
import java.util.jar.JarEntry;
4640
import java.util.jar.JarFile;
4741

4842
import static java.util.Objects.requireNonNull;
43+
import static java.util.stream.Collectors.toList;
4944

5045
@RequiredArgsConstructor
5146
public class MavenRecipeBundleReader implements RecipeBundleReader {
@@ -65,16 +60,25 @@ public class MavenRecipeBundleReader implements RecipeBundleReader {
6560
public RecipeMarketplace read() {
6661
if (recipeJar == null) {
6762
for (ResolvedDependency resolvedDependency : mrr.getDependencies().get(Scope.Runtime)) {
68-
if (resolvedDependency.isDirect() && recipeJar != null) {
63+
if (isResolvedBundle(resolvedDependency)) {
6964
recipeJar = downloader.downloadArtifact(resolvedDependency);
65+
break;
7066
}
7167
}
7268
if (recipeJar != null) {
7369
try (JarFile jarFile = new JarFile(recipeJar.toFile())) {
7470
JarEntry entry = jarFile.getJarEntry("META-INF/rewrite/recipes.csv");
7571
if (entry != null) {
7672
try (InputStream recipesCsv = jarFile.getInputStream(entry)) {
77-
return new RecipeMarketplaceReader().fromCsv(recipesCsv);
73+
RecipeMarketplace marketplace = new RecipeMarketplaceReader().fromCsv(recipesCsv);
74+
for (RecipeListing recipe : marketplace.getAllRecipes()) {
75+
// The recipes.csv inside a JAR may be generated without a version,
76+
// since the version of a published Maven artifact is determined at
77+
// publish time if the artifact is a snapshot. Having resolved the
78+
// JAR containing the recipes.csv, we now know the version.
79+
recipe.getBundle().setVersion(bundle.getVersion());
80+
}
81+
return marketplace;
7882
}
7983
}
8084
} catch (IOException e) {
@@ -92,10 +96,20 @@ public RecipeMarketplace read() {
9296
*/
9397
private RecipeMarketplace marketplaceFromClasspathScan() {
9498
String[] ga = bundle.getPackageName().split(":");
99+
RecipeMarketplace marketplace = new RecipeMarketplace();
100+
101+
// Scan only the target jar for recipes (using scanJar with jar name filter)
102+
List<Path> classpath = classpath();
103+
Environment env = Environment.builder().scanJar(
104+
requireNonNull(recipeJar).toAbsolutePath(),
105+
classpath.stream().map(Path::toAbsolutePath).collect(toList()),
106+
RecipeClassLoader.forScanning(recipeJar, classpath)
107+
).build();
108+
109+
// Bundle version may be set in the environment() call above (as the JARs making up
110+
// the classpath are resolved)
95111
GroupArtifactVersion gav = new GroupArtifactVersion(ga[0], ga[1], bundle.getVersion());
96112

97-
RecipeMarketplace marketplace = new RecipeMarketplace();
98-
Environment env = environment();
99113
for (RecipeDescriptor descriptor : env.listRecipeDescriptors()) {
100114
marketplace.install(
101115
RecipeListing.fromDescriptor(descriptor, new RecipeBundle(
@@ -115,7 +129,7 @@ public RecipeDescriptor describe(RecipeListing listing) {
115129
@Override
116130
public Recipe prepare(RecipeListing listing, @Nullable Map<String, Object> options) {
117131
Recipe r = environment().activateRecipes(listing.getName());
118-
return applyOptions(r, options);
132+
return r.withOptions(options);
119133
}
120134

121135
private Environment environment() {
@@ -127,7 +141,7 @@ private Environment environment() {
127141
return environment;
128142
}
129143

130-
private ClassLoader classLoader() {
144+
public ClassLoader classLoader() {
131145
if (classLoader == null) {
132146
// Create an isolated classloader with controlled parent delegation
133147
// This ensures maximum isolation while still allowing necessary shared types
@@ -141,16 +155,16 @@ List<Path> classpath() {
141155
if (classpath == null) {
142156
classpath = new ArrayList<>();
143157
for (ResolvedDependency resolvedDependency : mrr.getDependencies().get(Scope.Runtime)) {
158+
if (recipeJar != null && isResolvedBundle(resolvedDependency)) {
159+
// recipeJar may be non-null if the listRecipes() method was previously
160+
// used and the recipe JAR contains a recipes.csv that didn't necessitate
161+
// the whole classpath to be scanned.
162+
classpath.add(recipeJar);
163+
continue;
164+
}
144165
Lock lock = DEPENDENCY_LOCKS.computeIfAbsent(resolvedDependency.getGav(), g -> new ReentrantLock());
145166
lock.lock();
146167
try {
147-
if (resolvedDependency.isDirect() && recipeJar != null) {
148-
// recipeJar may be non-null if the listRecipes() method was previously
149-
// used and the recipe JAR contains a recipes.csv that didn't necessitate
150-
// the whole classpath to be scanned.
151-
classpath.add(recipeJar);
152-
continue;
153-
}
154168
Path path = downloader.downloadArtifact(resolvedDependency);
155169
if (path == null) {
156170
throw new IllegalStateException("Unable to download dependency " + resolvedDependency.getGav());
@@ -167,37 +181,8 @@ List<Path> classpath() {
167181
return classpath;
168182
}
169183

170-
private <R extends Recipe> R applyOptions(R recipe, @Nullable Map<String, Object> options) {
171-
Map<String, Object> m = new HashMap<>();
172-
m.put("@c", recipe.getName());
173-
ObjectMapper objectMapper = JsonMapper.builder()
174-
.constructorDetector(ConstructorDetector.USE_PROPERTIES_BASED)
175-
.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES)
176-
.configure(MapperFeature.PROPAGATE_TRANSIENT_MARKER, true)
177-
.build()
178-
.registerModule(new ParameterNamesModule())
179-
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
180-
// This is necessary to allow setting options like `FindTags#xPath`, as Jackson otherwise only sees a `xpath`
181-
// property, which it derives from the `getXPath()` method generated by Lombok
182-
objectMapper.setVisibility(objectMapper.getSerializationConfig().getDefaultVisibilityChecker()
183-
.withFieldVisibility(JsonAutoDetect.Visibility.ANY));
184-
try {
185-
//noinspection unchecked
186-
R clone = (R) recipe.clone();
187-
if (options != null) {
188-
m.putAll(options);
189-
for (OptionDescriptor optionDescriptor : clone.getDescriptor().getOptions()) {
190-
Object value = options.get(optionDescriptor.getName());
191-
if (value instanceof String) {
192-
Map<String, Object> option = new HashMap<>();
193-
option.put("value", value);
194-
objectMapper.updateValue(optionDescriptor, option);
195-
}
196-
}
197-
}
198-
return objectMapper.updateValue(clone, m);
199-
} catch (IOException e) {
200-
throw new UncheckedIOException(e);
201-
}
184+
private boolean isResolvedBundle(ResolvedDependency resolvedDependency) {
185+
return resolvedDependency.isDirect() && bundle.getPackageName()
186+
.equals(resolvedDependency.getGroupId() + ":" + resolvedDependency.getArtifactId());
202187
}
203188
}

0 commit comments

Comments
 (0)