1515 */
1616package 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 ;
2518import lombok .Getter ;
2619import lombok .RequiredArgsConstructor ;
2720import org .jspecify .annotations .Nullable ;
2821import org .openrewrite .Recipe ;
2922import org .openrewrite .config .ClasspathScanningLoader ;
3023import org .openrewrite .config .Environment ;
31- import org .openrewrite .config .OptionDescriptor ;
3224import org .openrewrite .config .RecipeDescriptor ;
3325import org .openrewrite .marketplace .*;
3426import org .openrewrite .maven .tree .*;
3527import org .openrewrite .maven .utilities .MavenArtifactDownloader ;
3628
3729import java .io .IOException ;
3830import java .io .InputStream ;
39- import java .io .UncheckedIOException ;
4031import 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 ;
4236import java .util .concurrent .ConcurrentHashMap ;
4337import java .util .concurrent .locks .Lock ;
4438import java .util .concurrent .locks .ReentrantLock ;
4539import java .util .jar .JarEntry ;
4640import java .util .jar .JarFile ;
4741
4842import static java .util .Objects .requireNonNull ;
43+ import static java .util .stream .Collectors .toList ;
4944
5045@ RequiredArgsConstructor
5146public 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