This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch fatjar
in repository https://gitbox.apache.org/repos/asf/camel.git

commit a42347c3050c5c603abf07e64baa0c9267a4dde3
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Mar 11 11:38:07 2026 +0100

    CAMEL-23169: camel-jbang - Export to standalone should package fat-jar 
using camel-repacker-plugin which we use in camel-launcher and spring boot as 
well.
---
 .../apache/camel/language/jq/JqBuiltInFnTest.java  |  49 +++++++
 .../fatjar/FatJarPackageScanClassResolver.java     | 136 +++++++++++++++++++
 .../fatjar/FatJarPackageScanResourceResolver.java  | 150 +++++++++++++++++++++
 .../java/org/apache/camel/main/fatjar/Main.java    |  66 +++++++++
 .../src/main/resources/templates/main-pom.tmpl     |  27 ++--
 .../src/main/resources/templates/main.tmpl         |   2 +-
 6 files changed, 410 insertions(+), 20 deletions(-)

diff --git 
a/components/camel-jq/src/test/java/org/apache/camel/language/jq/JqBuiltInFnTest.java
 
b/components/camel-jq/src/test/java/org/apache/camel/language/jq/JqBuiltInFnTest.java
new file mode 100644
index 000000000000..6e30288a84b3
--- /dev/null
+++ 
b/components/camel-jq/src/test/java/org/apache/camel/language/jq/JqBuiltInFnTest.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.language.jq;
+
+import java.net.InetAddress;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.junit.jupiter.api.Test;
+
+public class JqBuiltInFnTest extends JqTestSupport {
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:start")
+                        .transform().jq(".foo = hostname")
+                        .to("mock:result");
+            }
+        };
+    }
+
+    @Test
+    public void testExpression() throws Exception {
+        InetAddress addr = InetAddress.getLocalHost();
+
+        getMockEndpoint("mock:result")
+                .expectedBodiesReceived(node("foo", addr.getHostName()));
+
+        template.sendBodyAndHeader("direct:start", node("foo", "bar"), 
"MyHeader", "MyValue");
+
+        MockEndpoint.assertIsSatisfied(context);
+    }
+}
diff --git 
a/core/camel-main/src/main/java/org/apache/camel/main/fatjar/FatJarPackageScanClassResolver.java
 
b/core/camel-main/src/main/java/org/apache/camel/main/fatjar/FatJarPackageScanClassResolver.java
new file mode 100644
index 000000000000..876e7c60707f
--- /dev/null
+++ 
b/core/camel-main/src/main/java/org/apache/camel/main/fatjar/FatJarPackageScanClassResolver.java
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.main.fatjar;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.jar.JarEntry;
+import java.util.jar.JarInputStream;
+
+import org.apache.camel.support.scan.DefaultPackageScanClassResolver;
+import org.apache.camel.util.IOHelper;
+import org.apache.camel.util.StringHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * An implementation of the {@code 
org.apache.camel.spi.PackageScanClassResolver} that is able to scan spring-boot 
fat
+ * jars to find classes contained also in nested jars.
+ */
+public class FatJarPackageScanClassResolver extends 
DefaultPackageScanClassResolver {
+    private static final Logger LOG = 
LoggerFactory.getLogger(FatJarPackageScanClassResolver.class);
+
+    private static final String SPRING_BOOT_CLASSIC_LIB_ROOT = "lib/";
+    private static final String SPRING_BOOT_BOOT_INF_LIB_ROOT = 
"BOOT-INF/lib/";
+    private static final String SPRING_BOOT_BOOT_INF_CLASSES_ROOT = 
"BOOT-INF/classes/";
+    private static final String SPRING_BOOT_WEB_INF_LIB_ROOT = "WEB-INF/lib/";
+    private static final String SPRING_BOOT_WEB_INF_CLASSES_ROOT = 
"WEB-INF/classes/";
+
+    @Override
+    protected List<String> doLoadJarClassEntries(InputStream stream, String 
urlPath) {
+        return doLoadJarClassEntries(stream, urlPath, true, true);
+    }
+
+    @Override
+    protected String parseUrlPath(URL url) {
+        String urlPath = url.getFile();
+
+        urlPath = URLDecoder.decode(urlPath, StandardCharsets.UTF_8);
+        if (LOG.isTraceEnabled()) {
+            LOG.trace("Decoded urlPath: {} with protocol: {}", urlPath, 
url.getProtocol());
+        }
+
+        String nested = "nested:";
+        if (urlPath.startsWith(nested)) {
+            try {
+                urlPath = (new URI(url.getFile())).getPath();
+                return StringHelper.before(urlPath, "!", urlPath);
+            } catch (URISyntaxException e) {
+                // ignore
+            }
+            if (urlPath.startsWith(nested)) {
+                urlPath = urlPath.substring(nested.length());
+                return StringHelper.before(urlPath, "!", urlPath);
+            }
+        }
+
+        return super.parseUrlPath(url);
+    }
+
+    protected List<String> doLoadJarClassEntries(
+            InputStream stream, String urlPath, boolean inspectNestedJars,
+            boolean closeStream) {
+        List<String> entries = new ArrayList<>();
+
+        JarInputStream jarStream = null;
+        try {
+            jarStream = new JarInputStream(stream);
+
+            JarEntry entry;
+            while ((entry = jarStream.getNextJarEntry()) != null) {
+                String name = entry.getName();
+
+                name = name.trim();
+                if (!entry.isDirectory() && name.endsWith(".class")) {
+                    entries.add(cleanupSpringBootClassName(name));
+                } else if (inspectNestedJars && !entry.isDirectory() && 
isSpringBootNestedJar(name)) {
+                    String nestedUrl = urlPath + "!/" + name;
+                    LOG.trace("Inspecting nested jar: {}", nestedUrl);
+
+                    List<String> nestedEntries = 
doLoadJarClassEntries(jarStream, nestedUrl, false, false);
+                    entries.addAll(nestedEntries);
+                }
+            }
+        } catch (IOException ioe) {
+            LOG.warn("Cannot search jar file '" + urlPath + " due to an 
IOException: " + ioe.getMessage()
+                     + ". This exception is ignored.",
+                    ioe);
+        } finally {
+            if (closeStream) {
+                // stream is left open when scanning nested jars, otherwise 
the fat jar stream gets closed
+                IOHelper.close(jarStream, urlPath, LOG);
+            }
+        }
+
+        return entries;
+    }
+
+    private boolean isSpringBootNestedJar(String name) {
+        // Supporting both versions of the packaging model
+        return name.endsWith(".jar") && 
(name.startsWith(SPRING_BOOT_CLASSIC_LIB_ROOT)
+                || name.startsWith(SPRING_BOOT_BOOT_INF_LIB_ROOT) || 
name.startsWith(SPRING_BOOT_WEB_INF_LIB_ROOT));
+    }
+
+    private String cleanupSpringBootClassName(String name) {
+        // Classes inside BOOT-INF/classes will be loaded by the new 
classloader as if they were in the root
+        if (name.startsWith(SPRING_BOOT_BOOT_INF_CLASSES_ROOT)) {
+            name = name.substring(SPRING_BOOT_BOOT_INF_CLASSES_ROOT.length());
+        }
+        if (name.startsWith(SPRING_BOOT_WEB_INF_CLASSES_ROOT)) {
+            name = name.substring(SPRING_BOOT_WEB_INF_CLASSES_ROOT.length());
+        }
+        return name;
+    }
+
+}
diff --git 
a/core/camel-main/src/main/java/org/apache/camel/main/fatjar/FatJarPackageScanResourceResolver.java
 
b/core/camel-main/src/main/java/org/apache/camel/main/fatjar/FatJarPackageScanResourceResolver.java
new file mode 100644
index 000000000000..7592bb1b94d2
--- /dev/null
+++ 
b/core/camel-main/src/main/java/org/apache/camel/main/fatjar/FatJarPackageScanResourceResolver.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.main.fatjar;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.jar.JarEntry;
+import java.util.jar.JarInputStream;
+
+import org.apache.camel.support.scan.DefaultPackageScanResourceResolver;
+import org.apache.camel.util.IOHelper;
+import org.apache.camel.util.StringHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * An implementation of the {@code 
org.apache.camel.spi.PackageScanResourceResolver} that is able to scan 
spring-boot
+ * fat jars to find resources contained also in nested jars.
+ */
+public class FatJarPackageScanResourceResolver extends 
DefaultPackageScanResourceResolver {
+    private static final Logger LOG = 
LoggerFactory.getLogger(FatJarPackageScanResourceResolver.class);
+
+    private static final String SPRING_BOOT_CLASSIC_LIB_ROOT = "lib/";
+    private static final String SPRING_BOOT_BOOT_INF_LIB_ROOT = 
"BOOT-INF/lib/";
+    private static final String SPRING_BOOT_BOOT_INF_CLASSES_ROOT = 
"BOOT-INF/classes/";
+    private static final String SPRING_BOOT_WEB_INF_LIB_ROOT = "WEB-INF/lib/";
+    private static final String SPRING_BOOT_WEB_INF_CLASSES_ROOT = 
"WEB-INF/classes/";
+
+    @Override
+    protected List<String> doLoadImplementationsInJar(
+            String packageName, InputStream stream, String urlPath, 
Predicate<String> filter) {
+        return doLoadImplementationsInJar(packageName, stream, urlPath, true, 
true, filter);
+    }
+
+    protected List<String> doLoadImplementationsInJar(
+            String packageName, InputStream stream, String urlPath,
+            boolean inspectNestedJars, boolean closeStream, Predicate<String> 
filter) {
+        List<String> entries = new ArrayList<>();
+
+        JarInputStream jarStream = null;
+        try {
+            jarStream = new JarInputStream(stream);
+
+            JarEntry entry;
+            while ((entry = jarStream.getNextJarEntry()) != null) {
+                String name = entry.getName().trim();
+                if (inspectNestedJars && !entry.isDirectory() && 
isSpringBootNestedJar(name)) {
+                    String nestedUrl = urlPath + "!/" + name;
+                    LOG.trace("Inspecting nested jar: {}", nestedUrl);
+                    List<String> nestedEntries = 
doLoadImplementationsInJar(packageName, jarStream, nestedUrl, false,
+                            false, filter);
+                    entries.addAll(nestedEntries);
+                } else if (!entry.isDirectory()) {
+                    boolean accept;
+                    if (filter != null) {
+                        // use filter to accept or not
+                        accept = filter.test(name);
+                    } else {
+                        // skip class files by default
+                        accept = !name.endsWith(".class");
+                    }
+                    if (accept) {
+                        name = cleanupSpringBootClassName(name);
+                        // name is FQN so it must start with package name
+                        if (name.startsWith(packageName)) {
+                            entries.add(name);
+                        }
+                    }
+                }
+            }
+        } catch (IOException ioe) {
+            LOG.warn("Cannot search jar file '" + urlPath + " due to an 
IOException: " + ioe.getMessage()
+                     + ". This exception is ignored.",
+                    ioe);
+        } finally {
+            if (closeStream) {
+                // stream is left open when scanning nested jars, otherwise 
the fat jar stream gets closed
+                IOHelper.close(jarStream, urlPath, LOG);
+            }
+        }
+
+        return entries;
+    }
+
+    @Override
+    protected String parseUrlPath(URL url) {
+        String urlPath = url.getFile();
+
+        urlPath = URLDecoder.decode(urlPath, StandardCharsets.UTF_8);
+        if (LOG.isTraceEnabled()) {
+            LOG.trace("Decoded urlPath: {} with protocol: {}", urlPath, 
url.getProtocol());
+        }
+
+        String nested = "nested:";
+        if (urlPath.startsWith(nested)) {
+            try {
+                urlPath = (new URI(url.getFile())).getPath();
+                return StringHelper.before(urlPath, "!", urlPath);
+            } catch (URISyntaxException e) {
+                // ignore
+            }
+            if (urlPath.startsWith(nested)) {
+                urlPath = urlPath.substring(nested.length());
+                return StringHelper.before(urlPath, "!", urlPath);
+            }
+        }
+
+        return super.parseUrlPath(url);
+    }
+
+    private boolean isSpringBootNestedJar(String name) {
+        // Supporting both versions of the packaging model
+        return name.endsWith(".jar") && 
(name.startsWith(SPRING_BOOT_CLASSIC_LIB_ROOT)
+                || name.startsWith(SPRING_BOOT_BOOT_INF_LIB_ROOT) || 
name.startsWith(SPRING_BOOT_WEB_INF_LIB_ROOT));
+    }
+
+    private String cleanupSpringBootClassName(String name) {
+        // Classes inside BOOT-INF/classes will be loaded by the new 
classloader as if they were in the root
+        if (name.startsWith(SPRING_BOOT_BOOT_INF_CLASSES_ROOT)) {
+            name = name.substring(SPRING_BOOT_BOOT_INF_CLASSES_ROOT.length());
+        }
+        if (name.startsWith(SPRING_BOOT_WEB_INF_CLASSES_ROOT)) {
+            name = name.substring(SPRING_BOOT_WEB_INF_CLASSES_ROOT.length());
+        }
+        return name;
+    }
+
+}
diff --git 
a/core/camel-main/src/main/java/org/apache/camel/main/fatjar/Main.java 
b/core/camel-main/src/main/java/org/apache/camel/main/fatjar/Main.java
new file mode 100644
index 000000000000..8c06376cb59c
--- /dev/null
+++ b/core/camel-main/src/main/java/org/apache/camel/main/fatjar/Main.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.main.fatjar;
+
+import org.apache.camel.CamelConfiguration;
+import org.apache.camel.CamelContext;
+import org.apache.camel.spi.PackageScanClassResolver;
+import org.apache.camel.spi.PackageScanResourceResolver;
+
+/**
+ * A Main class for booting up Camel in standalone mode packaged as far-jar 
using the camel-repacker-plugin.
+ */
+public class Main extends org.apache.camel.main.Main {
+
+    /**
+     * Camel main application
+     *
+     * It is recommended to use {@link org.apache.camel.main.Main#Main(Class)} 
to specify the main class.
+     */
+    public Main() {
+    }
+
+    /**
+     * Camel main application
+     *
+     * @param mainClass the main class
+     */
+    public Main(Class<?> mainClass) {
+        super(mainClass);
+    }
+
+    /**
+     * Camel main application
+     *
+     * @param mainClass            the main class
+     * @param configurationClasses additional camel configuration classes
+     */
+    @SafeVarargs
+    public Main(Class<?> mainClass, Class<? extends CamelConfiguration>... 
configurationClasses) {
+        super(mainClass, configurationClasses);
+    }
+
+    @Override
+    protected CamelContext createCamelContext() {
+        CamelContext context = super.createCamelContext();
+        
context.getCamelContextExtension().addContextPlugin(PackageScanClassResolver.class,
+                new FatJarPackageScanClassResolver());
+        
context.getCamelContextExtension().addContextPlugin(PackageScanResourceResolver.class,
+                new FatJarPackageScanResourceResolver());
+        return context;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/main-pom.tmpl 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/main-pom.tmpl
index 361e30aa519c..9c01813bd7e9 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/main-pom.tmpl
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/main-pom.tmpl
@@ -96,30 +96,19 @@
             </plugin>
             <!-- package as runner jar -->
             <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-assembly-plugin</artifactId>
-                <version>3.7.1</version>
-                <configuration>
-                    <descriptors>
-                        
<descriptor>src/main/resources/assembly/runner.xml</descriptor>
-                    </descriptors>
-                    <archive>
-                        <manifest>
-                            <mainClass>{{ .MainClassname }}</mainClass>
-                        </manifest>
-                        <manifestEntries>
-                            <Multi-Release>true</Multi-Release>
-                        </manifestEntries>
-                    </archive>
-                    <appendAssemblyId>false</appendAssemblyId>
-                </configuration>
+                <groupId>org.apache.camel</groupId>
+                <artifactId>camel-repackager-maven-plugin</artifactId>
+                <version>{{ .CamelVersion }}</version>
                 <executions>
                     <execution>
-                        <id>make-assembly</id>
+                        <id>repackage-executable</id>
                         <phase>package</phase>
                         <goals>
-                            <goal>single</goal>
+                            <goal>repackage</goal>
                         </goals>
+                        <configuration>
+                            <mainClass>{{ .MainClassname }}</mainClass>
+                        </configuration>
                     </execution>
                 </executions>
             </plugin>
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/main.tmpl 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/main.tmpl
index 52e141843ca6..edb65a0f7f13 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/main.tmpl
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/main.tmpl
@@ -1,6 +1,6 @@
 {{ .PackageName }}
 
-import org.apache.camel.main.Main;
+import org.apache.camel.main.fatjar.Main;
 
 public class {{ .MainClassname }} {
 

Reply via email to