This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch camel-4.22.x
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/camel-4.22.x by this push:
new 672178040e07 [camel-4.22.x] CAMEL-25116: camel-platform-http-main -
/q/download?classpath=true lists the resources of the application (#27212)
672178040e07 is described below
commit 672178040e07379f1b681c43b19204a03655a155
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 1 18:09:13 2026 +0200
[camel-4.22.x] CAMEL-25116: camel-platform-http-main -
/q/download?classpath=true lists the resources of the application (#27212)
* chore: camel-platform-http-main - Document listing files with /q/download
Document how the /q/download management endpoint lists and serves files.
Without a file name only route source files are listed; ?classpath=true
also lists classpath resources, an Ant-style pattern filters the list, and
Accept: text/html returns the list as links. /q/download/{name} resolves
from the classpath first, so unlisted classpath files can still be
downloaded. The classpath=true option was previously undocumented.
Closes #26982
Signed-off-by: Claus Ibsen <[email protected]>
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
(cherry picked from commit 6567486f108c6b0d13ee78c865df6bf4fe3e974f)
* 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)
---------
Signed-off-by: Claus Ibsen <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
.../camel/catalog/docs/platform-http-main.adoc | 38 ++++
.../src/main/docs/platform-http-main.adoc | 38 ++++
.../platform/http/main/ManagementHttpServer.java | 196 +++++++++++++++++++--
.../http/main/ManagementHttpServerTest.java | 56 ++++++
4 files changed, 309 insertions(+), 19 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 824d6fb7d15b..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
@@ -42,6 +42,44 @@ These features are as follows:
You configure these features in the `application.properties` file using the
`camel.server.xxx` and `camel.management.xxx` options.
+=== Downloading files
+
+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 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 the resources of the application
+curl http://localhost:8080/q/download?classpath=true
+----
+
+The list can be filtered with an Ant-style pattern as the file name, for
example to list only the properties files:
+
+[source,bash]
+----
+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, 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]
+----
+curl http://localhost:8080/q/download/application.properties
+----
+
== JWT authentication
The embedded HTTP server can validate JWT bearer tokens on incoming requests.
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 824d6fb7d15b..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
@@ -42,6 +42,44 @@ These features are as follows:
You configure these features in the `application.properties` file using the
`camel.server.xxx` and `camel.management.xxx` options.
+=== Downloading files
+
+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 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 the resources of the application
+curl http://localhost:8080/q/download?classpath=true
+----
+
+The list can be filtered with an Ant-style pattern as the file name, for
example to list only the properties files:
+
+[source,bash]
+----
+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, 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]
+----
+curl http://localhost:8080/q/download/application.properties
+----
+
== JWT authentication
The embedded HTTP server can validate JWT bearer tokens on incoming requests.
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());
+ }
+
}