Croway commented on code in PR #23129:
URL: https://github.com/apache/camel/pull/23129#discussion_r3225048229
##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginHelper.java:
##########
@@ -179,30 +221,247 @@ public static Map<String, Plugin>
getActivePlugins(CamelJBangMain main, String r
versionCheck(main, version, firstVersion, command);
}
- Optional<Plugin> plugin = getPlugin(command, version, gav,
repos, main.getOut());
- if (plugin.isPresent()) {
- activePlugins.put(command, plugin.get());
+ ResolveResult res = resolvePlugin(properties, command,
version, gav, repos, main.getOut());
+ if (res.plugin().isPresent()) {
+ activePlugins.put(command, res.plugin().get());
+ if (res.cacheWritten()) {
+ configDirty = true;
+ }
} else {
main.getOut().println("camel-jbang-plugin-" + command + "
not found. Exit");
main.quit(1);
}
}
+ if (configDirty) {
+ savePluginConfig(config);
+ }
}
return activePlugins;
}
public static Optional<Plugin> getPlugin(String name, String
defaultVersion, String gav, String repos, Printer printer) {
+ return resolvePlugin(null, name, defaultVersion, gav, repos,
printer).plugin();
+ }
+
+ /**
+ * Resolves a plugin by trying, in order: the cached metadata in the
plugin entry (fast path with no IO beyond
+ * size+mtime checks), the factory finder (embedded plugin on the JVM
classpath), and finally the Maven downloader.
+ * When the downloader runs, the resolved classpath is captured into the
plugin entry's {@code resolved} block so
+ * subsequent invocations take the fast path.
+ */
+ private static ResolveResult resolvePlugin(
+ JsonObject entry, String name, String defaultVersion, String gav,
String repos, Printer printer) {
+ Optional<Plugin> cached = loadFromCache(entry, defaultVersion, gav,
repos);
+ if (cached.isPresent()) {
+ return new ResolveResult(cached, false);
+ }
+
Optional<Plugin> plugin =
FACTORY_FINDER.newInstance("camel-jbang-plugin-" + name, Plugin.class);
- if (plugin.isEmpty()) {
- final MavenGav mavenGav = dependencyAsMavenGav(gav);
- final String group = extractGroup(mavenGav, "org.apache.camel");
- final String depVersion = extractVersion(mavenGav, defaultVersion);
+ if (plugin.isPresent()) {
+ return new ResolveResult(plugin, false);
+ }
+
+ final MavenGav mavenGav = dependencyAsMavenGav(gav);
+ final String group = extractGroup(mavenGav, "org.apache.camel");
+ final String depVersion = extractVersion(mavenGav, defaultVersion);
+
+ DownloadResult dr = downloadPlugin(name, defaultVersion, depVersion,
group, repos, printer);
+ boolean cacheWritten = false;
+ if (dr.plugin().isPresent() && entry != null && dr.classLoader() !=
null && dr.className() != null) {
+ cacheWritten = writeCache(entry, defaultVersion, gav, repos,
dr.className(), dr.classLoader(), name, depVersion);
+ }
+ return new ResolveResult(dr.plugin(), cacheWritten);
+ }
+
+ private static Optional<Plugin> loadFromCache(JsonObject entry, String
camelVersion, String gav, String repos) {
+ if (entry == null) {
+ return Optional.empty();
+ }
+ JsonObject resolved = entry.getMap("resolved");
+ if (resolved == null) {
+ return Optional.empty();
+ }
+ if (!sameCamelVersion(asString(resolved.get("camelVersion")),
camelVersion)) {
+ return Optional.empty();
+ }
+ if (!nullSafeEquals(normalize(asString(resolved.get("gav"))),
normalize(gav))) {
+ return Optional.empty();
+ }
+ if (!nullSafeEquals(normalize(asString(resolved.get("repos"))),
normalize(repos))) {
+ return Optional.empty();
+ }
+ String className = asString(resolved.get("className"));
+ if (className == null || className.isBlank()) {
+ return Optional.empty();
+ }
+ Object cpObj = resolved.get("classpath");
+ if (!(cpObj instanceof Collection)) {
+ return Optional.empty();
+ }
+ Collection<?> classpath = (Collection<?>) cpObj;
+ if (classpath.isEmpty()) {
+ return Optional.empty();
+ }
+
+ List<URL> urls = new ArrayList<>(classpath.size());
+ for (Object o : classpath) {
+ if (!(o instanceof Map)) {
+ return Optional.empty();
+ }
+ Map<?, ?> jar = (Map<?, ?>) o;
+ Path p = validateFileEntry(jar);
+ if (p == null) {
+ return Optional.empty();
+ }
+ try {
+ urls.add(p.toUri().toURL());
+ } catch (IOException e) {
+ return Optional.empty();
+ }
+ }
+
+ // If the cache tracks the plugin POM, validate it too. Detects
POM-only changes (e.g. a SNAPSHOT
+ // plugin's transitive deps changed without a jar rebuild).
+ Object pomObj = resolved.get("pom");
+ if (pomObj instanceof Map) {
+ if (validateFileEntry((Map<?, ?>) pomObj) == null) {
+ return Optional.empty();
+ }
+ }
+
+ try {
+ URLClassLoader cl = new URLClassLoader(urls.toArray(new URL[0]),
PluginHelper.class.getClassLoader());
+ Class<?> pluginClass = cl.loadClass(className);
+ Plugin instance = (Plugin) ObjectHelper.newInstance(pluginClass);
+ instance.setClassLoader(cl);
+ return Optional.of(instance);
+ } catch (Exception e) {
+ return Optional.empty();
+ }
+ }
+
+ /**
+ * Persists the resolved plugin classpath into the entry's {@code
resolved} block. Package-private so unit tests can
+ * drive the happy path without invoking the Maven downloader. Also tracks
the plugin's own POM file (size+mtime) so
+ * a POM-only change (e.g. a SNAPSHOT plugin gaining a new transitive
dependency without a jar rebuild) invalidates
+ * the cache on the next invocation.
+ */
+ static boolean writeCache(
+ JsonObject entry, String camelVersion, String gav, String repos,
String className, ClassLoader cl,
+ String pluginCommand, String pluginVersion) {
+ URL[] urls;
+ if (cl instanceof URLClassLoader) {
+ urls = ((URLClassLoader) cl).getURLs();
+ } else {
+ return false;
+ }
+ if (urls == null || urls.length == 0) {
+ return false;
+ }
+ Collection<JsonObject> classpath = new ArrayList<>(urls.length);
+ JsonObject pomEntry = null;
+ String pluginJarName = "camel-jbang-plugin-" + pluginCommand + "-" +
pluginVersion + ".jar";
+ for (URL u : urls) {
+ try {
+ Path p = Path.of(u.toURI());
+ if (!Files.exists(p)) {
+ return false;
+ }
+ JsonObject jar = new JsonObject();
+ jar.put("path", p.toAbsolutePath().toString());
+ jar.put("size", Files.size(p));
+ jar.put("mtime", Files.getLastModifiedTime(p).toMillis());
+ classpath.add(jar);
+
+ // Identify the plugin's own jar by filename and track the
sibling POM, so a Maven re-install
+ // of the plugin (which always rewrites the POM) is detected
even when the jar bytes happen
+ // to be unchanged.
+ if (pomEntry == null &&
pluginJarName.equals(p.getFileName().toString())) {
+ Path pom = p.resolveSibling("camel-jbang-plugin-" +
pluginCommand + "-" + pluginVersion + ".pom");
+ if (Files.exists(pom)) {
+ pomEntry = new JsonObject();
+ pomEntry.put("path", pom.toAbsolutePath().toString());
+ pomEntry.put("size", Files.size(pom));
+ pomEntry.put("mtime",
Files.getLastModifiedTime(pom).toMillis());
+ }
+ }
+ } catch (Exception e) {
+ return false;
+ }
+ }
+ JsonObject resolved = new JsonObject();
+ resolved.put("camelVersion", camelVersion);
+ if (normalize(gav) != null) {
+ resolved.put("gav", normalize(gav));
+ }
+ if (normalize(repos) != null) {
+ resolved.put("repos", normalize(repos));
+ }
+ resolved.put("className", className);
+ resolved.put("cachedAt", System.currentTimeMillis());
+ resolved.put("classpath", classpath);
+ if (pomEntry != null) {
+ resolved.put("pom", pomEntry);
+ }
+ entry.put("resolved", resolved);
+ return true;
+ }
+
+ /**
+ * Validates a {path, size, mtime} entry from the cache against the actual
file on disk. Returns the resolved Path
+ * on match, or null if the file is missing, was modified, or the entry is
malformed.
+ */
+ private static Path validateFileEntry(Map<?, ?> entry) {
+ String path = asString(entry.get("path"));
+ Object sizeObj = entry.get("size");
+ Object mtimeObj = entry.get("mtime");
+ if (path == null || !(sizeObj instanceof Number) || !(mtimeObj
instanceof Number)) {
+ return null;
+ }
+ long size = ((Number) sizeObj).longValue();
+ long mtime = ((Number) mtimeObj).longValue();
+ Path p = Path.of(path);
+ try {
+ if (!Files.exists(p) || Files.size(p) != size ||
Files.getLastModifiedTime(p).toMillis() != mtime) {
+ return null;
+ }
+ return p;
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ private static boolean sameCamelVersion(String a, String b) {
+ return stripSnapshot(a).equals(stripSnapshot(b));
+ }
- plugin = downloadPlugin(name, defaultVersion, depVersion, group,
repos, printer);
+ private static String stripSnapshot(String v) {
+ if (v == null) {
+ return "";
}
+ return v.endsWith("-SNAPSHOT") ? v.substring(0, v.length() -
"-SNAPSHOT".length()) : v;
+ }
+
+ private static String normalize(String s) {
+ if (s == null || s.isBlank()) {
+ return null;
+ }
+ return s.trim();
+ }
+
+ private static boolean nullSafeEquals(String a, String b) {
Review Comment:
I think this can be removed, and Objects.equals(a, b) can be used instead
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]