Skip to content

Commit 0e021e8

Browse files
authored
Clean up the error message when a recipe cannot be found in the marketplace (#6688)
* Clean up the error message when a recipe cannot be found in the marketplace * Fix missing license * Add caching support * Add license header and remove unused imports
1 parent 9fe1fb5 commit 0e021e8

3 files changed

Lines changed: 169 additions & 2 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2026 the original author or authors.
3+
* <p>
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite;
17+
18+
import lombok.EqualsAndHashCode;
19+
import lombok.Value;
20+
import org.jspecify.annotations.Nullable;
21+
22+
import java.net.URI;
23+
24+
@Value
25+
@EqualsAndHashCode(callSuper = false)
26+
public class RecipeNotFoundException extends RuntimeException {
27+
String recipeName;
28+
29+
@Nullable
30+
URI source;
31+
32+
public RecipeNotFoundException(String recipeName) {
33+
this(recipeName, null);
34+
}
35+
36+
public RecipeNotFoundException(String recipeName, @Nullable URI source) {
37+
super("Unable to find recipe " + recipeName + (source == null ? "" : " listed in " + source));
38+
this.recipeName = recipeName;
39+
this.source = source;
40+
}
41+
}

rewrite-core/src/main/java/org/openrewrite/config/YamlResourceLoader.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ public YamlResourceLoader(InputStream yamlInput, URI source, Properties properti
9898
this.recipeLoader = (recipeName, options) -> {
9999
RecipeListing listing = marketplace.findRecipe(recipeName);
100100
if (listing == null) {
101-
throw new IllegalStateException("Unable to find recipe " + recipeName + " listed in " + source);
101+
throw new RecipeNotFoundException(recipeName, source);
102102
}
103103
return listing.prepare(resolvers, options == null ? emptyMap() : options);
104104
};
@@ -356,6 +356,12 @@ void loadRecipe(@Language("markdown") String name,
356356
} catch (IllegalArgumentException ignored) {
357357
// it's probably declarative
358358
addLazyLoadRecipe.accept(recipeName);
359+
} catch (RecipeNotFoundException e) {
360+
addInvalidRecipeValidation(
361+
addValidation,
362+
recipeName,
363+
null,
364+
e.getMessage());
359365
} catch (NoClassDefFoundError e) {
360366
addInvalidRecipeValidation(
361367
addValidation,
@@ -385,6 +391,12 @@ void loadRecipe(@Language("markdown") String name,
385391
recipeArgs,
386392
"Unable to load Recipe: " + e);
387393
}
394+
} catch (RecipeNotFoundException e) {
395+
addInvalidRecipeValidation(
396+
addValidation,
397+
recipeName,
398+
recipeArgs,
399+
e.getMessage());
388400
} catch (NoClassDefFoundError e) {
389401
addInvalidRecipeValidation(
390402
addValidation,
@@ -404,7 +416,7 @@ void loadRecipe(@Language("markdown") String name,
404416
addValidation,
405417
recipeName,
406418
recipeArgs,
407-
"Unexpected declarative recipe parsing exception " + e.getClass().getName());
419+
"Unexpected declarative recipe parsing exception " + e.getClass().getName() + ": " + e.getMessage());
408420
}
409421
} else {
410422
addValidation.accept(invalid(
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Copyright 2026 the original author or authors.
3+
* <p>
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.maven.marketplace;
17+
18+
import lombok.RequiredArgsConstructor;
19+
import org.openrewrite.ExecutionContext;
20+
import org.openrewrite.marketplace.RecipeBundle;
21+
import org.openrewrite.marketplace.RecipeBundleReader;
22+
import org.openrewrite.marketplace.RecipeBundleResolver;
23+
import org.openrewrite.marketplace.RecipeClassLoaderFactory;
24+
import org.openrewrite.maven.utilities.MavenArtifactDownloader;
25+
26+
import java.util.HashMap;
27+
import java.util.Map;
28+
import java.util.Objects;
29+
import java.util.concurrent.atomic.AtomicInteger;
30+
31+
@RequiredArgsConstructor
32+
public class CachingMavenRecipeBundleResolver implements RecipeBundleResolver {
33+
private final ExecutionContext ctx;
34+
private final MavenArtifactDownloader downloader;
35+
private final RecipeClassLoaderFactory classLoaderFactory;
36+
private final Map<String, ResolverEntry> resolverCache = new HashMap<>();
37+
38+
@Override
39+
public String getEcosystem() {
40+
return "maven";
41+
}
42+
43+
@Override
44+
public RecipeBundleReader resolve(RecipeBundle bundle) {
45+
try (RecipeBundleResolver resolver = resolverFor(bundle)) {
46+
return resolver.resolve(bundle);
47+
} catch (Exception e) {
48+
throw new RuntimeException(e);
49+
}
50+
}
51+
52+
private String resolverKey(RecipeBundle bundle) {
53+
return String.format("%s:%s:%s", bundle.getTeam(), bundle.getPackageEcosystem(), bundle.getPackageName());
54+
}
55+
56+
public synchronized RecipeBundleResolver resolverFor(RecipeBundle bundle) {
57+
String key = resolverKey(bundle);
58+
ResolverEntry entry = resolverCache.get(key);
59+
if (entry != null && !entry.evicted) {
60+
if (Objects.equals(entry.bundle.getVersion(), bundle.getVersion())) {
61+
return entry.createProxyResolver();
62+
}
63+
64+
entry.markEvicted();
65+
}
66+
67+
entry = new ResolverEntry(bundle, new MavenRecipeBundleResolver(ctx, downloader, classLoaderFactory));
68+
resolverCache.put(key, entry);
69+
return entry.createProxyResolver();
70+
}
71+
72+
@RequiredArgsConstructor
73+
private static class ResolverEntry {
74+
private final RecipeBundle bundle;
75+
private final RecipeBundleResolver resolver;
76+
private final AtomicInteger leases = new AtomicInteger();
77+
private boolean evicted;
78+
79+
private synchronized void markEvicted() {
80+
evicted = true;
81+
if (leases.get() == 0) {
82+
try {
83+
resolver.close();
84+
} catch (Exception e) {
85+
throw new RuntimeException(e);
86+
}
87+
}
88+
}
89+
90+
RecipeBundleResolver createProxyResolver() {
91+
leases.incrementAndGet();
92+
return new RecipeBundleResolver() {
93+
@Override
94+
public String getEcosystem() {
95+
return resolver.getEcosystem();
96+
}
97+
98+
@Override
99+
public RecipeBundleReader resolve(RecipeBundle bundle) {
100+
return resolver.resolve(bundle);
101+
}
102+
103+
@Override
104+
public void close() throws Exception {
105+
synchronized (ResolverEntry.this) {
106+
if (leases.decrementAndGet() == 0 && evicted) {
107+
resolver.close();
108+
}
109+
}
110+
}
111+
};
112+
}
113+
}
114+
}

0 commit comments

Comments
 (0)