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 }} {
