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());
+    }
+
 }

Reply via email to