This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/CAMEL-25116-4.22.x in repository https://gitbox.apache.org/repos/asf/camel.git
commit dde44e43682f38926753a547313430538e038fba Author: Claus Ibsen <[email protected]> AuthorDate: Mon Sep 28 23:21:18 2026 +0200 CAMEL-25116: camel-platform-http-main - /q/download?classpath=true lists the resources of the application (#27024) Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> (cherry picked from commit 5204e4957608f2d83c713aa89a5609f8383bbfa2) --- .../camel/catalog/docs/platform-http-main.adoc | 11 +- .../src/main/docs/platform-http-main.adoc | 11 +- .../platform/http/main/ManagementHttpServer.java | 196 +++++++++++++++++++-- .../http/main/ManagementHttpServerTest.java | 56 ++++++ 4 files changed, 247 insertions(+), 27 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/platform-http-main.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/platform-http-main.adoc index 56622c5c085f..bf00766a9894 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/platform-http-main.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/platform-http-main.adoc @@ -48,14 +48,17 @@ File download is enabled with `camel.management.downloadEnabled=true`. Calling `/q/download` without a file name returns a list of the files that can be downloaded. By default, the list only contains the source files of the routes (such as `*.camel.yaml` files). -To also list the resources from the classpath, add the `classpath=true` query parameter: +To also list the resources of the application, add the `classpath=true` query parameter. +These are the resources from the classpath directories (such as `target/classes`) and from the application JAR +when the application is packaged, and when using `camel run` the files from the project directory +(such as `application.properties` and `openapi.json`). The content of the dependency JARs is not included. [source,bash] ---- # list route source files curl http://localhost:8080/q/download -# list route source files and all classpath resources +# list route source files and the resources of the application curl http://localhost:8080/q/download?classpath=true ---- @@ -69,8 +72,8 @@ curl http://localhost:8080/q/download/*.properties?classpath=true When the `Accept` header includes `html` (such as from a web browser), the list is returned as HTML links. To download a file, call `/q/download/` followed by the file name. The file is resolved from the classpath -first, and then from the route source files. A file on the classpath can be downloaded even if it is not listed -(when not using `classpath=true`). +first, then from the route source files, and when using `camel run` from the project directory. +A file on the classpath can be downloaded even if it is not listed (when not using `classpath=true`). [source,bash] ---- diff --git a/components/camel-platform-http-main/src/main/docs/platform-http-main.adoc b/components/camel-platform-http-main/src/main/docs/platform-http-main.adoc index 56622c5c085f..bf00766a9894 100644 --- a/components/camel-platform-http-main/src/main/docs/platform-http-main.adoc +++ b/components/camel-platform-http-main/src/main/docs/platform-http-main.adoc @@ -48,14 +48,17 @@ File download is enabled with `camel.management.downloadEnabled=true`. Calling `/q/download` without a file name returns a list of the files that can be downloaded. By default, the list only contains the source files of the routes (such as `*.camel.yaml` files). -To also list the resources from the classpath, add the `classpath=true` query parameter: +To also list the resources of the application, add the `classpath=true` query parameter. +These are the resources from the classpath directories (such as `target/classes`) and from the application JAR +when the application is packaged, and when using `camel run` the files from the project directory +(such as `application.properties` and `openapi.json`). The content of the dependency JARs is not included. [source,bash] ---- # list route source files curl http://localhost:8080/q/download -# list route source files and all classpath resources +# list route source files and the resources of the application curl http://localhost:8080/q/download?classpath=true ---- @@ -69,8 +72,8 @@ curl http://localhost:8080/q/download/*.properties?classpath=true When the `Accept` header includes `html` (such as from a web browser), the list is returned as HTML links. To download a file, call `/q/download/` followed by the file name. The file is resolved from the classpath -first, and then from the route source files. A file on the classpath can be downloaded even if it is not listed -(when not using `classpath=true`). +first, then from the route source files, and when using `camel run` from the project directory. +A file on the classpath can be downloaded even if it is not listed (when not using `classpath=true`). [source,bash] ---- diff --git a/components/camel-platform-http-main/src/main/java/org/apache/camel/component/platform/http/main/ManagementHttpServer.java b/components/camel-platform-http-main/src/main/java/org/apache/camel/component/platform/http/main/ManagementHttpServer.java index b04e9b4fa80b..c648a8ccc4ad 100644 --- a/components/camel-platform-http-main/src/main/java/org/apache/camel/component/platform/http/main/ManagementHttpServer.java +++ b/components/camel-platform-http-main/src/main/java/org/apache/camel/component/platform/http/main/ManagementHttpServer.java @@ -21,12 +21,21 @@ import java.io.FileOutputStream; import java.io.IOException; import java.lang.management.ManagementFactory; import java.lang.management.RuntimeMXBean; +import java.net.JarURLConnection; +import java.net.URI; +import java.net.URL; +import java.net.URLConnection; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -34,6 +43,7 @@ import java.util.StringJoiner; import java.util.TreeSet; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.Stream; import io.vertx.core.Handler; import io.vertx.core.http.HttpMethod; @@ -67,7 +77,6 @@ import org.apache.camel.health.HealthCheckHelper; import org.apache.camel.health.HealthCheckRegistry; import org.apache.camel.http.base.HttpProtocolHeaderFilterStrategy; import org.apache.camel.spi.HeaderFilterStrategy; -import org.apache.camel.spi.PackageScanResourceResolver; import org.apache.camel.spi.ReloadStrategy; import org.apache.camel.spi.Resource; import org.apache.camel.spi.ResourceLoader; @@ -99,6 +108,8 @@ import org.slf4j.LoggerFactory; public class ManagementHttpServer extends ServiceSupport implements CamelContextAware, StaticService { private static final Logger LOG = LoggerFactory.getLogger(ManagementHttpServer.class); + // the files camel run adds to the classpath + private static final String CLASSPATH_FILES = "camel.jbang.classpathFiles"; private static final int BODY_MAX_CHARS = 128 * 1024; private static final int DEFAULT_POLL_TIMEOUT = 20000; @@ -805,30 +816,27 @@ public class ManagementHttpServer extends ServiceSupport implements CamelContext if (name == null || name.isBlank() || matcher.isPattern(name)) { Set<String> names = new TreeSet<>(); if (cp) { - // also look inside classpath - PackageScanResourceResolver resolver = PluginHelper.getPackageScanResourceResolver(camelContext); - resolver.addClassLoader(camelContext.getApplicationContextClassLoader()); - try { - String pattern = "**/*"; - if (name != null && !name.isBlank()) { - pattern = "**/" + name; - } - for (Resource res : resolver.findResources(pattern)) { - String loc = res.getLocation(); - loc = LoggerHelper.sourceNameOnly(loc); - names.add(loc); + // also include the resources of the application itself + String pattern = "**/*"; + if (name != null && !name.isBlank()) { + pattern = "**/" + name; + } + for (String n : findApplicationResources()) { + if (matcher.match(pattern, n)) { + names.add(n); } - } catch (Exception e) { - // ignore } } // always include routes for (org.apache.camel.Route route : camelContext.getRoutes()) { String loc = route.getSourceLocation(); if (loc != null) { - loc = LoggerHelper.sourceNameOnly(loc); - if (name == null || name.isBlank() || matcher.match(name, loc)) { - names.add(loc); + final String n = sourceName(loc); + // skip routes from a classpath directory that are already listed as a resource + boolean listed = cp && FileUtil.isAbsolute(new File(n)) + && names.stream().anyMatch(r -> n.endsWith("/" + r)); + if (!listed && (name == null || name.isBlank() || matcher.match(name, n))) { + names.add(n); } } } @@ -867,7 +875,7 @@ public class ManagementHttpServer extends ServiceSupport implements CamelContext for (org.apache.camel.Route route : camelContext.getRoutes()) { String loc = route.getSourceLocation(); if (loc != null) { - loc = LoggerHelper.sourceNameOnly(loc); + loc = sourceName(loc); if (matcher.match(name, loc)) { res = route.getSourceResource(); break; @@ -875,6 +883,15 @@ public class ManagementHttpServer extends ServiceSupport implements CamelContext } } } + if (res == null || !res.exists()) { + // files from the project directory (such as application.properties) when using camel run + for (String f : findProjectFiles()) { + if (name.equals(f)) { + res = loader.resolveResource("file:" + f); + break; + } + } + } if (res != null && res.exists()) { ctx.response().putHeader("Content-Type", "application/octet-stream"); ctx.response().putHeader("Content-Disposition", @@ -901,6 +918,147 @@ public class ManagementHttpServer extends ServiceSupport implements CamelContext null, "text/plain,application/octet-stream", null); } + /** + * The name of the source such as a route file, which for a packaged application is the path inside the JAR. + */ + private static String sourceName(String location) { + String answer = LoggerHelper.sourceNameOnly(location); + int pos = answer.lastIndexOf("!/"); + if (pos != -1) { + answer = answer.substring(pos + 2); + } + return answer; + } + + /** + * Finds the resources of the application itself, that is the classpath directories and the classes folder of a + * packaged JAR, and the files from the project directory when using camel run. The content of dependency JARs is + * not included. + */ + protected Set<String> findApplicationResources() { + Set<String> answer = new TreeSet<>(); + // the application context classloader may not be the classloader of a packaged application + Set<ClassLoader> loaders = new LinkedHashSet<>(); + if (camelContext.getApplicationContextClassLoader() != null) { + loaders.add(camelContext.getApplicationContextClassLoader()); + } + if (Thread.currentThread().getContextClassLoader() != null) { + loaders.add(Thread.currentThread().getContextClassLoader()); + } + loaders.add(ManagementHttpServer.class.getClassLoader()); + // the base package of the application (such as the package of the main class) is used to find the JAR of + // a packaged application, as a JAR is not returned as a classpath root + String base = camelContext.getCamelContextExtension().getBasePackageScan(); + String basePath = base != null && !base.isBlank() ? base.replace('.', '/') + "/" : null; + Map<String, Boolean> roots = new LinkedHashMap<>(); + for (ClassLoader cl : loaders) { + try { + for (URL url : Collections.list(cl.getResources(""))) { + roots.putIfAbsent(url.toExternalForm(), false); + } + if (basePath != null) { + for (URL url : Collections.list(cl.getResources(basePath))) { + String s = url.toExternalForm(); + int pos = s.lastIndexOf("!/"); + if (pos != -1) { + roots.put(s.substring(0, pos + 2), true); + } else if (s.endsWith(basePath)) { + roots.putIfAbsent(s.substring(0, s.length() - basePath.length()), false); + } + } + } + } catch (Exception e) { + LOG.debug("Error finding application resources due to: {}. This exception is ignored.", e.getMessage(), + e); + } + } + for (Map.Entry<String, Boolean> root : roots.entrySet()) { + try { + for (String n : listResourceRoot(URI.create(root.getKey()).toURL(), root.getValue())) { + if (isApplicationResource(n)) { + answer.add(n); + } + } + } catch (Exception e) { + LOG.debug("Error listing resources in: {} due to: {}. This exception is ignored.", root.getKey(), + e.getMessage(), e); + } + } + answer.addAll(findProjectFiles()); + return answer; + } + + private static List<String> listResourceRoot(URL url, boolean applicationJar) throws Exception { + List<String> answer = new ArrayList<>(); + if ("file".equals(url.getProtocol())) { + Path root = Path.of(url.toURI()); + if (Files.isDirectory(root)) { + try (Stream<Path> files = Files.walk(root)) { + files.filter(Files::isRegularFile) + .map(p -> FileUtil.normalizePath(root.relativize(p).toString()).replace('\\', '/')) + .forEach(answer::add); + } + } + } else if ("jar".equals(url.getProtocol())) { + // the root of a dependency JAR is not part of the application, but the classes folder of a packaged + // application (such as jar:nested:/app.jar/!BOOT-INF/classes/!/) and the application JAR are + String s = url.toExternalForm(); + if (s.endsWith("!/")) { + s = s.substring(0, s.length() - 2); + } + if (s.endsWith(".jar") && !applicationJar) { + return answer; + } + URLConnection con = url.openConnection(); + if (con instanceof JarURLConnection jar) { + String prefix = jar.getEntryName() != null ? jar.getEntryName() : ""; + // the JAR file is cached by the JDK so it must not be closed + jar.getJarFile().stream() + .filter(e -> !e.isDirectory() && e.getName().startsWith(prefix)) + .map(e -> e.getName().substring(prefix.length())) + .forEach(answer::add); + } + } + return answer; + } + + /** + * The files from the project directory that camel run has added to the classpath or loaded as properties. + */ + private List<String> findProjectFiles() { + List<String> answer = new ArrayList<>(); + List<String> files = new ArrayList<>(); + camelContext.getPropertiesComponent().resolveProperty(CLASSPATH_FILES) + .ifPresent(s -> files.addAll(Arrays.asList(s.split(",")))); + for (String loc : camelContext.getPropertiesComponent().getLocations()) { + loc = StringHelper.before(loc, ";", loc); + if (loc.startsWith("file:")) { + files.add(loc.substring(5)); + } + } + // only files inside the project directory + Path dir = Path.of("").toAbsolutePath().normalize(); + for (String f : files) { + f = f.trim(); + Path p = dir.resolve(f).normalize(); + if (!f.isEmpty() && p.startsWith(dir) && Files.isRegularFile(p)) { + String n = FileUtil.normalizePath(dir.relativize(p).toString()).replace('\\', '/'); + if (isApplicationResource(n)) { + answer.add(n); + } + } + } + return answer; + } + + /** + * Whether the file is a resource that can be listed and downloaded, which excludes classes and JARs (camel run can + * add both to the classpath) and META-INF files. + */ + private static boolean isApplicationResource(String name) { + return !name.endsWith(".class") && !name.endsWith(".jar") && !name.startsWith("META-INF/"); + } + protected void setupSendConsole() { final Route send = router.route("/q/send/") .produces("application/json") diff --git a/components/camel-platform-http-main/src/test/java/org/apache/camel/component/platform/http/main/ManagementHttpServerTest.java b/components/camel-platform-http-main/src/test/java/org/apache/camel/component/platform/http/main/ManagementHttpServerTest.java index 7d451dab1238..106be95dc381 100644 --- a/components/camel-platform-http-main/src/test/java/org/apache/camel/component/platform/http/main/ManagementHttpServerTest.java +++ b/components/camel-platform-http-main/src/test/java/org/apache/camel/component/platform/http/main/ManagementHttpServerTest.java @@ -21,6 +21,7 @@ import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.util.List; import org.apache.camel.CamelContext; import org.apache.camel.impl.DefaultCamelContext; @@ -29,6 +30,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; class ManagementHttpServerTest { @@ -64,4 +67,57 @@ class ManagementHttpServerTest { } + @Test + public void downloadListsApplicationResources() throws Exception { + ManagementHttpServer server = new ManagementHttpServer(); + + camelContext = new DefaultCamelContext(); + // files that camel run adds to the classpath, only files inside the project directory are included + String clazz = "target/classes/" + ManagementHttpServer.class.getName().replace('.', '/') + ".class"; + camelContext.getPropertiesComponent().addInitialProperty("camel.jbang.classpathFiles", + "pom.xml,../pom.xml," + clazz); + server.setCamelContext(camelContext); + + server.setHost("0.0.0.0"); + server.setPort(port.getPort()); + server.setPath("/"); + server.setDownloadEnabled(true); + server.start(); + try { + HttpResponse<String> response = get("/q/download?classpath=true"); + assertEquals(200, response.statusCode()); + List<String> names = response.body().lines().toList(); + // resources from the classpath + assertTrue(names.contains("log4j2.properties"), names.toString()); + assertTrue(names.contains("basic-auth.properties"), names.toString()); + // files from the project directory + assertTrue(names.contains("pom.xml"), names.toString()); + assertFalse(names.contains("../pom.xml"), names.toString()); + assertFalse(names.contains(clazz), names.toString()); + // no classes or content of dependency JARs + assertTrue(names.stream().noneMatch(n -> n.endsWith(".class")), names.toString()); + assertTrue(names.stream().noneMatch(n -> n.startsWith("META-INF/")), names.toString()); + + response = get("/q/download/*.jks?classpath=true"); + assertEquals("test-camel-main-auth-jwt.jks", response.body()); + + response = get("/q/download/pom.xml"); + assertEquals(200, response.statusCode()); + assertTrue(response.body().contains("<artifactId>camel-platform-http-main</artifactId>")); + + // classes cannot be downloaded + response = get("/q/download/" + clazz); + assertEquals(204, response.statusCode()); + } finally { + server.stop(); + } + } + + private HttpResponse<String> get(String path) throws IOException, InterruptedException { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + port.getPort() + path)) + .build(); + return HttpClient.newBuilder().build().send(request, HttpResponse.BodyHandlers.ofString()); + } + }
