This is an automated email from the ASF dual-hosted git repository. jamesnetherton pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git
The following commit(s) were added to refs/heads/main by this push: new 988fd126ca Support Camel default route resource locations 988fd126ca is described below commit 988fd126ca9d3419e2998772a01169d134558e21 Author: James Netherton <jamesnether...@gmail.com> AuthorDate: Mon Mar 24 12:38:21 2025 +0000 Support Camel default route resource locations Fixes #6733 --- .../deployment/CamelRouteResourcesProcessor.java | 92 ++++++++++++++ .../core/deployment/main/CamelMainHelper.java | 8 +- .../main/CamelMainHotDeploymentProcessor.java | 54 +++----- .../main/CamelMainNativeImageProcessor.java | 34 ++--- .../spi/CamelRouteResourceBuildItem.java | 89 +++++++++++++ .../quarkus/core/deployment/util/CamelSupport.java | 36 ++++++ .../camel/quarkus/core/CamelCapabilities.java | 1 + .../CamelQuarkusPackageScanResourceResolver.java | 122 +++++++++++++++++- extensions-core/yaml-dsl/runtime/pom.xml | 5 + .../java/deployment/OpenApiJavaProcessor.java | 6 +- .../apache/camel/quarkus/core/CoreResource.java | 16 +++ .../core/src/main/resources/application.properties | 2 +- .../sub-resources-folder/foo/bar/test-1.txt | 7 +- .../sub-resources-folder/foo/bar/test-2.txt | 7 +- .../org/apache/camel/quarkus/core/CoreTest.java | 57 +++++++++ integration-tests/main-devmode/pom.xml | 17 +++ ...ludePatternWithDefaultLocationsDevModeTest.java | 141 +++++++++++++++++++++ .../camel/quarkus/main/CamelSupportResource.java | 9 ++ .../src/main/resources/application.properties | 1 - .../resources/{routes => camel-rest}/my-rests.yaml | 0 .../{routes => camel-template}/my-templates.yaml | 0 .../resources/{routes => camel}/my-routes.yaml | 0 22 files changed, 622 insertions(+), 82 deletions(-) diff --git a/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/CamelRouteResourcesProcessor.java b/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/CamelRouteResourcesProcessor.java new file mode 100644 index 0000000000..eb116a29f3 --- /dev/null +++ b/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/CamelRouteResourcesProcessor.java @@ -0,0 +1,92 @@ +/* + * 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.quarkus.core.deployment; + +import java.util.Set; +import java.util.stream.Collectors; + +import io.quarkus.deployment.ApplicationArchive; +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.ApplicationArchivesBuildItem; +import io.quarkus.maven.dependency.ResolvedDependency; +import io.quarkus.paths.PathVisit; +import io.quarkus.paths.PathVisitor; +import org.apache.camel.quarkus.core.deployment.main.CamelMainHelper; +import org.apache.camel.quarkus.core.deployment.spi.CamelRouteResourceBuildItem; +import org.apache.camel.quarkus.core.deployment.util.CamelSupport; +import org.apache.camel.util.FileUtil; + +import static org.apache.camel.quarkus.core.deployment.util.CamelSupport.CLASSPATH_PREFIX; + +class CamelRouteResourcesProcessor { + @BuildStep + void camelRouteResources( + Capabilities capabilities, + ApplicationArchivesBuildItem applicationArchives, + BuildProducer<CamelRouteResourceBuildItem> routeResource) { + + if (CamelSupport.isRouteResourceDslCapabilitiesPresent(capabilities)) { + // Classpath route resources + Set<String> classpathIncludePatterns = CamelMainHelper.routesIncludePattern() + .filter(pattern -> !pattern.contains(":") || pattern.startsWith(CLASSPATH_PREFIX)) + .map(CamelSupport::stripClasspathScheme) + .collect(Collectors.toSet()); + + if (!classpathIncludePatterns.isEmpty()) { + Set<String> routeResourceFileExtensions = CamelSupport.getRouteResourceFileExtensions(capabilities); + io.quarkus.paths.PathFilter pathFilter = io.quarkus.paths.PathFilter.forIncludes(classpathIncludePatterns); + + // Search the root application archive for routes + ApplicationArchive rootArchive = applicationArchives.getRootArchive(); + ResolvedDependency rootArchiveDependency = rootArchive.getResolvedDependency(); + PathVisitor rootArchivePathVisitor = createPathVisitor(routeResourceFileExtensions, routeResource, true); + rootArchiveDependency.getContentTree(pathFilter).walk(rootArchivePathVisitor); + + // Search other application archive for routes + PathVisitor appArchivePathVisitor = createPathVisitor(routeResourceFileExtensions, routeResource, false); + for (ApplicationArchive archive : applicationArchives.getApplicationArchives()) { + ResolvedDependency dependency = archive.getResolvedDependency(); + dependency.getContentTree(pathFilter).walk(appArchivePathVisitor); + } + } + + // External file route resources + CamelMainHelper.routesIncludePattern() + .filter(pattern -> pattern.startsWith("file:")) + .map(CamelRouteResourceBuildItem::new) + .forEach(routeResource::produce); + } + } + + private PathVisitor createPathVisitor( + Set<String> routeResourceFileExtensions, + BuildProducer<CamelRouteResourceBuildItem> routeResource, + boolean isHotReloadable) { + return new PathVisitor() { + @Override + public void visitPath(PathVisit visit) { + String path = visit.getPath().toString(); + String extension = FileUtil.onlyExt(path); + if (routeResourceFileExtensions.contains(extension)) { + routeResource.produce(new CamelRouteResourceBuildItem(CLASSPATH_PREFIX + path, isHotReloadable)); + } + } + }; + } +} diff --git a/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/main/CamelMainHelper.java b/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/main/CamelMainHelper.java index 716bbd2b9b..26e077ec2d 100644 --- a/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/main/CamelMainHelper.java +++ b/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/main/CamelMainHelper.java @@ -17,7 +17,6 @@ package org.apache.camel.quarkus.core.deployment.main; import java.util.function.Consumer; -import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.camel.impl.engine.DefaultPackageScanResourceResolver; @@ -26,6 +25,8 @@ import org.apache.camel.spi.Resource; import org.apache.camel.util.AntPathMatcher; public final class CamelMainHelper { + private static final String[] DEFAULT_ROUTES_INCLUDE_PATTERN = { "classpath:camel/*", + "classpath:camel-template/*", "classpath:camel-rest/*" }; private static final String[] EMPTY_STRING_ARRAY = new String[0]; private CamelMainHelper() { @@ -36,9 +37,8 @@ public final class CamelMainHelper { "camel.main.routes-include-pattern", String[].class, EMPTY_STRING_ARRAY); final String[] i2 = CamelSupport.getOptionalConfigValue( "camel.main.routesIncludePattern", String[].class, EMPTY_STRING_ARRAY); - return i1.length == 0 && i2.length == 0 - ? Stream.empty() + ? Stream.of(DEFAULT_ROUTES_INCLUDE_PATTERN) : Stream.concat(Stream.of(i1), Stream.of(i2)).filter(location -> !"false".equals(location)); } @@ -62,7 +62,7 @@ public final class CamelMainHelper { try (DefaultPackageScanResourceResolver resolver = new DefaultPackageScanResourceResolver()) { resolver.setCamelContext(CamelSupport.newBuildTimeCamelContext(true)); String[] excludes = routesExcludePattern().toArray(String[]::new); - for (String include : routesIncludePattern().collect(Collectors.toList())) { + for (String include : routesIncludePattern().toList()) { for (Resource resource : resolver.findResources(include)) { if (AntPathMatcher.INSTANCE.anyMatch(excludes, resource.getLocation())) { return; diff --git a/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/main/CamelMainHotDeploymentProcessor.java b/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/main/CamelMainHotDeploymentProcessor.java index fa8b0d313b..f70465e0eb 100644 --- a/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/main/CamelMainHotDeploymentProcessor.java +++ b/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/main/CamelMainHotDeploymentProcessor.java @@ -16,53 +16,35 @@ */ package org.apache.camel.quarkus.core.deployment.main; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.List; -import java.util.stream.Collectors; +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.camel.quarkus.core.deployment.spi.CamelRouteResourceBuildItem; +import org.jboss.logging.Logger; +@BuildSteps(onlyIf = { IsDevelopment.class }) class CamelMainHotDeploymentProcessor { - private static final Logger LOGGER = LoggerFactory.getLogger(CamelMainHotDeploymentProcessor.class); - private static final String FILE_PREFIX = "file:"; - private static final String CLASSPATH_PREFIX = "classpath:"; + private static final Logger LOGGER = Logger.getLogger(CamelMainHotDeploymentProcessor.class); @BuildStep - List<HotDeploymentWatchedFileBuildItem> locations() { - List<HotDeploymentWatchedFileBuildItem> items = CamelMainHelper.routesIncludePattern() - .map(CamelMainHotDeploymentProcessor::routesIncludePatternToLocation) - .filter(location -> location != null) + void locations( + List<CamelRouteResourceBuildItem> camelRouteResources, + BuildProducer<HotDeploymentWatchedFileBuildItem> hotDeploymentWatchedFile) { + + camelRouteResources.stream() + .filter(CamelRouteResourceBuildItem::isHotReloadable) + .map(CamelRouteResourceBuildItem::getSourcePath) + .peek(location -> LOGGER.debugf("Configuring watched file %s", location)) .distinct() .map(HotDeploymentWatchedFileBuildItem::new) - .collect(Collectors.toList()); - - if (!items.isEmpty()) { - LOGGER.info("HotDeployment files:"); - for (HotDeploymentWatchedFileBuildItem item : items) { - LOGGER.info("- {}", item.getLocation()); - } - } - - return items; - } + .forEach(hotDeploymentWatchedFile::produce); - private static String routesIncludePatternToLocation(String pattern) { - if (pattern.startsWith(CLASSPATH_PREFIX)) { - return pattern.substring(CLASSPATH_PREFIX.length()); - } else if (pattern.startsWith(FILE_PREFIX)) { - String filePattern = pattern.substring(FILE_PREFIX.length()); - Path filePatternPath = Paths.get(filePattern); - if (Files.exists(filePatternPath)) { - return filePatternPath.toAbsolutePath().toString(); - } - } else if (pattern.length() > 0) { - return pattern; + if (!camelRouteResources.isEmpty()) { + LOGGER.info("Camel routes live reload enabled"); } - return null; } } diff --git a/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/main/CamelMainNativeImageProcessor.java b/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/main/CamelMainNativeImageProcessor.java index a0ecbe6462..b8e9559b27 100644 --- a/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/main/CamelMainNativeImageProcessor.java +++ b/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/main/CamelMainNativeImageProcessor.java @@ -16,21 +16,16 @@ */ package org.apache.camel.quarkus.core.deployment.main; -import java.util.stream.Collectors; +import java.util.List; import java.util.stream.Stream; -import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; -import org.apache.camel.support.ResourceHelper; -import org.apache.camel.util.AntPathMatcher; -import org.jboss.logging.Logger; +import org.apache.camel.quarkus.core.deployment.spi.CamelRouteResourceBuildItem; public class CamelMainNativeImageProcessor { - private static final Logger LOG = Logger.getLogger(CamelMainNativeImageProcessor.class); - @BuildStep void reflectiveCLasses(BuildProducer<ReflectiveClassBuildItem> producer) { // TODO: The classes below are needed to fix https://github.com/apache/camel-quarkus/issues/1005 @@ -43,26 +38,15 @@ public class CamelMainNativeImageProcessor { } @BuildStep - private void camelNativeImageResources( - Capabilities capabilities, + void camelNativeImageResources( + List<CamelRouteResourceBuildItem> camelRouteResources, BuildProducer<NativeImageResourceBuildItem> nativeResource) { - for (String path : CamelMainHelper.routesIncludePattern().collect(Collectors.toList())) { - String scheme = ResourceHelper.getScheme(path); - - // Null scheme is equivalent to classpath scheme - if (scheme == null || scheme.equals("classpath:")) { - if (AntPathMatcher.INSTANCE.isPattern(path)) { - // Classpath directory traversal via wildcard paths does not work on GraalVM. - // The exact path to the resource has to be looked up - // https://github.com/oracle/graal/issues/1108 - LOG.warnf("Classpath wildcards does not work in native mode. Resources matching %s will not be loaded.", - path); - } else { - nativeResource.produce(new NativeImageResourceBuildItem(path.replace("classpath:", ""))); - } - } - } + camelRouteResources.stream() + .filter(CamelRouteResourceBuildItem::isClasspathResource) + .map(CamelRouteResourceBuildItem::getSourcePath) + .map(NativeImageResourceBuildItem::new) + .forEach(nativeResource::produce); String[] resources = Stream.of("components", "dataformats", "languages") .map(k -> "org/apache/camel/main/" + k + ".properties") diff --git a/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/spi/CamelRouteResourceBuildItem.java b/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/spi/CamelRouteResourceBuildItem.java new file mode 100644 index 0000000000..c9850ff9dc --- /dev/null +++ b/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/spi/CamelRouteResourceBuildItem.java @@ -0,0 +1,89 @@ +/* + * 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.quarkus.core.deployment.spi; + +import java.nio.file.Paths; +import java.util.Objects; + +import io.quarkus.builder.item.MultiBuildItem; +import org.apache.camel.quarkus.core.util.FileUtils; +import org.apache.camel.spi.Resource; +import org.apache.camel.util.FileUtil; +import org.apache.camel.util.StringHelper; + +import static org.apache.camel.quarkus.core.deployment.util.CamelSupport.CLASSPATH_PREFIX; + +/** + * Holds a {@link Resource} relating to discovered Camel DSL route definition files defined by + * route inclusion patterns configuration. + */ +public final class CamelRouteResourceBuildItem extends MultiBuildItem { + private static final String GRADLE_RESOURCES_PATH = "build/resources/main/"; + private static final String MAVEN_CLASSES_PATH = "target/classes/"; + private final String location; + private final String sourcePath; + private final boolean isHotReloadable; + + public CamelRouteResourceBuildItem(String location) { + this(location, true); + } + + public CamelRouteResourceBuildItem(String location, boolean isHotReloadable) { + Objects.requireNonNull(location, "location cannot be null"); + this.location = FileUtils.nixifyPath(location); + this.sourcePath = computeSourcePath(); + this.isHotReloadable = isHotReloadable; + } + + public String getLocation() { + return location; + } + + public String getSourcePath() { + return sourcePath; + } + + public boolean isHotReloadable() { + return isHotReloadable; + } + + public boolean isClasspathResource() { + return location.startsWith(CLASSPATH_PREFIX) + || location.contains(MAVEN_CLASSES_PATH) + || location.contains(GRADLE_RESOURCES_PATH); + } + + private String computeSourcePath() { + String result = StringHelper.after(location, ":", location); + + if (location.startsWith("file:")) { + return Paths.get(result).toAbsolutePath().toString(); + } + + result = FileUtil.stripLeadingSeparator(result); + + if (location.contains(MAVEN_CLASSES_PATH)) { + result = StringHelper.after(location, MAVEN_CLASSES_PATH); + } + + if (location.contains(GRADLE_RESOURCES_PATH)) { + result = StringHelper.after(location, GRADLE_RESOURCES_PATH); + } + + return result; + } +} diff --git a/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/util/CamelSupport.java b/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/util/CamelSupport.java index a267617fed..3429a0b215 100644 --- a/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/util/CamelSupport.java +++ b/extensions-core/core/deployment/src/main/java/org/apache/camel/quarkus/core/deployment/util/CamelSupport.java @@ -30,10 +30,12 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import io.quarkus.deployment.ApplicationArchive; +import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.builditem.ApplicationArchivesBuildItem; import org.apache.camel.CamelContext; import org.apache.camel.impl.DefaultCamelContext; import org.apache.camel.impl.engine.AbstractCamelContext; +import org.apache.camel.quarkus.core.CamelCapabilities; import org.apache.camel.quarkus.core.deployment.spi.CamelServiceBuildItem; import org.eclipse.microprofile.config.ConfigProvider; import org.jboss.jandex.ClassInfo; @@ -41,6 +43,7 @@ import org.jboss.jandex.ClassInfo; public final class CamelSupport { public static final String CAMEL_SERVICE_BASE_PATH = "META-INF/services/org/apache/camel"; public static final String CAMEL_ROOT_PACKAGE_DIRECTORY = "org/apache/camel"; + public static final String CLASSPATH_PREFIX = "classpath:"; public static final String COMPILATION_JVM_TARGET = "17"; private CamelSupport() { @@ -139,4 +142,37 @@ public final class CamelSupport { } return context; } + + public static String stripClasspathScheme(String path) { + Objects.requireNonNull(path, "path must not be null"); + if (path.startsWith(CLASSPATH_PREFIX)) { + return path.substring(CLASSPATH_PREFIX.length()); + } + return path; + } + + public static boolean isRouteResourceDslCapabilitiesPresent(Capabilities capabilities) { + return capabilities.isPresent(CamelCapabilities.XML_JAXB) + || capabilities.isPresent(CamelCapabilities.XML_IO_DSL) + || capabilities.isPresent(CamelCapabilities.YAML_DSL) + || capabilities.isPresent(CamelCapabilities.JAVA_JOOR_DSL); + } + + public static Set<String> getRouteResourceFileExtensions(Capabilities capabilities) { + Set<String> extensions = new HashSet<>(); + if (capabilities.isPresent(CamelCapabilities.XML_JAXB) || capabilities.isPresent(CamelCapabilities.XML_IO_DSL)) { + extensions.add("xml"); + } + + if (capabilities.isPresent(CamelCapabilities.YAML_DSL)) { + extensions.add("yaml"); + extensions.add("yml"); + } + + if (capabilities.isPresent(CamelCapabilities.JAVA_JOOR_DSL)) { + extensions.add("java"); + } + + return extensions; + } } diff --git a/extensions-core/core/runtime/src/main/java/org/apache/camel/quarkus/core/CamelCapabilities.java b/extensions-core/core/runtime/src/main/java/org/apache/camel/quarkus/core/CamelCapabilities.java index a625416c79..9f3697d542 100644 --- a/extensions-core/core/runtime/src/main/java/org/apache/camel/quarkus/core/CamelCapabilities.java +++ b/extensions-core/core/runtime/src/main/java/org/apache/camel/quarkus/core/CamelCapabilities.java @@ -26,6 +26,7 @@ public final class CamelCapabilities { public static final String XML_IO_DSL = "org.apache.camel.xml.io.dsl"; public static final String XML_JAXB = "org.apache.camel.xml.jaxb"; public static final String XML_JAXP = "org.apache.camel.xml.jaxp"; + public static final String YAML_DSL = "org.apache.camel.yaml.dsl"; private CamelCapabilities() { } diff --git a/extensions-core/core/runtime/src/main/java/org/apache/camel/quarkus/core/CamelQuarkusPackageScanResourceResolver.java b/extensions-core/core/runtime/src/main/java/org/apache/camel/quarkus/core/CamelQuarkusPackageScanResourceResolver.java index 7d7dde7972..798d7892c5 100644 --- a/extensions-core/core/runtime/src/main/java/org/apache/camel/quarkus/core/CamelQuarkusPackageScanResourceResolver.java +++ b/extensions-core/core/runtime/src/main/java/org/apache/camel/quarkus/core/CamelQuarkusPackageScanResourceResolver.java @@ -16,16 +16,136 @@ */ package org.apache.camel.quarkus.core; +import java.io.IOException; +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.FileSystemAlreadyExistsException; +import java.nio.file.FileSystems; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Collection; +import java.util.Collections; +import java.util.stream.Stream; + +import io.quarkus.runtime.ImageMode; import org.apache.camel.impl.engine.DefaultPackageScanResourceResolver; import org.apache.camel.spi.PackageScanResourceResolver; +import org.apache.camel.spi.Resource; +import org.apache.camel.spi.ResourceLoader; +import org.apache.camel.support.PluginHelper; +import org.apache.camel.support.ResourceHelper; +import org.apache.camel.util.AntPathMatcher; +import org.apache.camel.util.ObjectHelper; +import org.jboss.logging.Logger; /** * Custom {@link PackageScanResourceResolver} that adds the specific {@link ClassLoader} instance - * that Camel Quarkus requires for resolving resources. + * that Camel Quarkus requires for resolving resources. Also performs native image capable classpath glob pattern + * resource resolution. */ public class CamelQuarkusPackageScanResourceResolver extends DefaultPackageScanResourceResolver { + private static final Logger LOG = Logger.getLogger(CamelQuarkusPackageScanResourceResolver.class); + private static final URI NATIVE_IMAGE_FILESYSTEM_URI = URI.create("resource:/"); + private FileSystem resourceFileSystem; + @Override public void initialize() { addClassLoader(Thread.currentThread().getContextClassLoader()); } + + @Override + protected void doStop() throws Exception { + if (resourceFileSystem != null) { + resourceFileSystem.close(); + resourceFileSystem = null; + } + } + + @Override + public Collection<Resource> findResources(String location) throws Exception { + Collection<Resource> resources = super.findResources(location); + + // If no matches were found for the location pattern in native mode, try to use the resource scheme filesystem + if (resources.isEmpty() && ImageMode.current().isNativeImage()) { + String scheme = ResourceHelper.getScheme(location); + if (isClassPathPattern(location, scheme)) { + FileSystem fileSystem = getNativeImageResourceFileSystem(); + ResourceLoader resourceLoader = PluginHelper.getResourceLoader(getCamelContext()); + String root = AntPathMatcher.INSTANCE.determineRootDir(location); + String rootWithoutScheme = scheme != null ? root.substring(scheme.length()) : root; + String subPattern = location.substring(root.length()); + Path startPath = fileSystem.getPath(rootWithoutScheme); + + if (!Files.exists(startPath)) { + LOG.tracef("Failed to find resources for location: %s as path %s does not exist", location, startPath); + return resources; + } + + LOG.tracef("Finding native resources for location: %s, sub pattern %s, under path %s", location, subPattern, + rootWithoutScheme); + + // Iterate classpath resources in the native application and try to find matches + Files.walkFileTree(startPath, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + Path pathToMatch = file.getNameCount() > 1 ? file.subpath(1, file.getNameCount()) : file.getFileName(); + + LOG.tracef("Checking for native resource match for: %s", pathToMatch); + if (AntPathMatcher.INSTANCE.match(subPattern, pathToMatch.toString())) { + LOG.tracef("Matched native resource: %s with pattern: %s", file, subPattern); + resources.add(resourceLoader.resolveResource(file.toString())); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) { + LOG.tracef(exc, "Failed to process resource: %s", file); + return FileVisitResult.CONTINUE; + } + }); + + // Fallback on matching files explicitly in the resolved directory path + if (resources.isEmpty() && Files.isDirectory(startPath)) { + try (Stream<Path> files = Files.list(startPath)) { + files.map(Path::getFileName) + .map(Path::toString) + .filter(path -> AntPathMatcher.INSTANCE.match(subPattern, path)) + .map(path -> startPath.resolve(path).toString()) + .peek(path -> LOG.tracef("Matched native resource: %s with pattern: %s", path, subPattern)) + .map(resourceLoader::resolveResource) + .forEach(resources::add); + } + } + } + } + + return resources; + } + + protected boolean isClassPathPattern(String location, String scheme) { + if (ObjectHelper.isNotEmpty(location)) { + return (AntPathMatcher.INSTANCE.isPattern(location)) + && (ObjectHelper.isEmpty(scheme) || "classpath:".equals(scheme)); + } + return false; + } + + protected FileSystem getNativeImageResourceFileSystem() { + // Must lazy init the FileSystem at runtime so that resources are discoverable + if (resourceFileSystem == null) { + try { + resourceFileSystem = FileSystems.newFileSystem(NATIVE_IMAGE_FILESYSTEM_URI, + Collections.singletonMap("create", "true")); + } catch (FileSystemAlreadyExistsException ex) { + resourceFileSystem = FileSystems.getFileSystem(NATIVE_IMAGE_FILESYSTEM_URI); + } catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + return resourceFileSystem; + } } diff --git a/extensions-core/yaml-dsl/runtime/pom.xml b/extensions-core/yaml-dsl/runtime/pom.xml index 9c1f4b9760..2187736cb7 100644 --- a/extensions-core/yaml-dsl/runtime/pom.xml +++ b/extensions-core/yaml-dsl/runtime/pom.xml @@ -51,6 +51,11 @@ <plugin> <groupId>io.quarkus</groupId> <artifactId>quarkus-extension-maven-plugin</artifactId> + <configuration> + <capabilities> + <provides>org.apache.camel.yaml.dsl</provides> + </capabilities> + </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> diff --git a/extensions/openapi-java/deployment/src/main/java/org/apache/camel/quarkus/component/openapi/java/deployment/OpenApiJavaProcessor.java b/extensions/openapi-java/deployment/src/main/java/org/apache/camel/quarkus/component/openapi/java/deployment/OpenApiJavaProcessor.java index 77fd10d25e..981edaf5b2 100644 --- a/extensions/openapi-java/deployment/src/main/java/org/apache/camel/quarkus/component/openapi/java/deployment/OpenApiJavaProcessor.java +++ b/extensions/openapi-java/deployment/src/main/java/org/apache/camel/quarkus/component/openapi/java/deployment/OpenApiJavaProcessor.java @@ -24,6 +24,7 @@ import java.util.Optional; import java.util.function.BiConsumer; import java.util.function.BooleanSupplier; import java.util.function.Consumer; +import java.util.stream.Collectors; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonFactory; @@ -55,6 +56,7 @@ import org.apache.camel.openapi.DefaultRestDefinitionsResolver; import org.apache.camel.openapi.RestDefinitionsResolver; import org.apache.camel.openapi.RestOpenApiReader; import org.apache.camel.openapi.RestOpenApiSupport; +import org.apache.camel.quarkus.core.deployment.main.CamelMainHelper; import org.apache.camel.quarkus.core.deployment.spi.CamelRoutesBuilderClassBuildItem; import org.apache.camel.quarkus.core.deployment.util.CamelSupport; import org.apache.camel.spi.RestConfiguration; @@ -97,9 +99,9 @@ class OpenApiJavaProcessor { configurer.setRoutesBuilders(routes); configurer.setRoutesCollector(new DefaultRoutesCollector()); configurer.setRoutesIncludePattern( - CamelSupport.getOptionalConfigValue("camel.main.routes-include-pattern", String.class, null)); + CamelMainHelper.routesIncludePattern().collect(Collectors.joining(","))); configurer.setRoutesExcludePattern( - CamelSupport.getOptionalConfigValue("camel.main.routes-exclude-pattern", String.class, null)); + CamelMainHelper.routesExcludePattern().collect(Collectors.joining(","))); final CamelContext ctx = CamelSupport.newBuildTimeCamelContext(true); if (!routesBuilderClasses.isEmpty()) { diff --git a/integration-test-groups/foundation/core/src/main/java/org/apache/camel/quarkus/core/CoreResource.java b/integration-test-groups/foundation/core/src/main/java/org/apache/camel/quarkus/core/CoreResource.java index a4fb5e45c2..2bbffd7ad5 100644 --- a/integration-test-groups/foundation/core/src/main/java/org/apache/camel/quarkus/core/CoreResource.java +++ b/integration-test-groups/foundation/core/src/main/java/org/apache/camel/quarkus/core/CoreResource.java @@ -35,6 +35,7 @@ import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.apache.camel.CamelContext; @@ -43,8 +44,11 @@ import org.apache.camel.NoSuchLanguageException; import org.apache.camel.catalog.RuntimeCamelCatalog; import org.apache.camel.impl.engine.DefaultHeadersMapFactory; import org.apache.camel.model.ModelCamelContext; +import org.apache.camel.quarkus.core.util.FileUtils; import org.apache.camel.spi.Registry; +import org.apache.camel.spi.Resource; import org.apache.camel.support.LRUCacheFactory; +import org.apache.camel.support.PluginHelper; import org.apache.camel.support.startup.DefaultStartupStepRecorder; import org.jboss.logging.Logger; @@ -284,4 +288,16 @@ public class CoreResource { return ((MySerializationObject) is.readObject()).isCorrect(); } + @Path("/resource/resolve") + @GET + @Produces(MediaType.TEXT_PLAIN) + public String resolveResource(@QueryParam("path") String path) throws Exception { + return PluginHelper.getPackageScanResourceResolver(context) + .findResources(path) + .stream() + .filter(Resource::exists) + .map(Resource::getLocation) + .map(FileUtils::nixifyPath) + .collect(Collectors.joining(",")); + } } diff --git a/integration-test-groups/foundation/core/src/main/resources/application.properties b/integration-test-groups/foundation/core/src/main/resources/application.properties index 96499830f5..a46d41abaa 100644 --- a/integration-test-groups/foundation/core/src/main/resources/application.properties +++ b/integration-test-groups/foundation/core/src/main/resources/application.properties @@ -27,7 +27,7 @@ quarkus.camel.native.reflection.exclude-patterns = org.apache.commons.lang3.tupl # Quarkus :: Camel # quarkus.camel.runtime-catalog.languages = false -quarkus.native.resources.includes = mysimple.txt,include-pattern-folder/* +quarkus.native.resources.includes = mysimple.txt,include-pattern-folder/*,sub-resources-folder/foo/bar/* quarkus.native.resources.excludes = exclude-pattern-folder/*,include-pattern-folder/excluded.txt quarkus.camel.native.reflection.serialization-enabled = true diff --git a/integration-tests/main-yaml/src/main/resources/application.properties b/integration-test-groups/foundation/core/src/main/resources/sub-resources-folder/foo/bar/test-1.txt similarity index 86% copy from integration-tests/main-yaml/src/main/resources/application.properties copy to integration-test-groups/foundation/core/src/main/resources/sub-resources-folder/foo/bar/test-1.txt index 9abe188ba9..db0337dd5c 100644 --- a/integration-tests/main-yaml/src/main/resources/application.properties +++ b/integration-test-groups/foundation/core/src/main/resources/sub-resources-folder/foo/bar/test-1.txt @@ -14,9 +14,4 @@ ## See the License for the specific language governing permissions and ## limitations under the License. ## --------------------------------------------------------------------------- - -# -# Main -# -camel.main.routes-include-pattern = routes/my-routes.yaml,routes/my-rests.yaml,routes/my-templates.yaml -camel.main.dump-routes = yaml +Test 1 \ No newline at end of file diff --git a/integration-tests/main-yaml/src/main/resources/application.properties b/integration-test-groups/foundation/core/src/main/resources/sub-resources-folder/foo/bar/test-2.txt similarity index 86% copy from integration-tests/main-yaml/src/main/resources/application.properties copy to integration-test-groups/foundation/core/src/main/resources/sub-resources-folder/foo/bar/test-2.txt index 9abe188ba9..8f10373c75 100644 --- a/integration-tests/main-yaml/src/main/resources/application.properties +++ b/integration-test-groups/foundation/core/src/main/resources/sub-resources-folder/foo/bar/test-2.txt @@ -14,9 +14,4 @@ ## See the License for the specific language governing permissions and ## limitations under the License. ## --------------------------------------------------------------------------- - -# -# Main -# -camel.main.routes-include-pattern = routes/my-routes.yaml,routes/my-rests.yaml,routes/my-templates.yaml -camel.main.dump-routes = yaml +Test 2 \ No newline at end of file diff --git a/integration-test-groups/foundation/core/src/test/java/org/apache/camel/quarkus/core/CoreTest.java b/integration-test-groups/foundation/core/src/test/java/org/apache/camel/quarkus/core/CoreTest.java index bd7cc598b6..3c69d00ccc 100644 --- a/integration-test-groups/foundation/core/src/test/java/org/apache/camel/quarkus/core/CoreTest.java +++ b/integration-test-groups/foundation/core/src/test/java/org/apache/camel/quarkus/core/CoreTest.java @@ -137,4 +137,61 @@ public class CoreTest { void testSerialization() { RestAssured.when().get("/core/serialization").then().body(is("true")); } + + @Test + void classpathPackageScan() { + // Path without scheme + RestAssured.given() + .queryParam("path", "include-pattern-folder/included.txt") + .get("/core/resource/resolve") + .then() + .body(endsWith("include-pattern-folder/included.txt")); + + // Classpath scheme + RestAssured.given() + .queryParam("path", "classpath:include-pattern-folder/included.txt") + .get("/core/resource/resolve") + .then() + .body(endsWith("include-pattern-folder/included.txt")); + + // Classpath globbing + RestAssured.given() + .queryParam("path", "sub-resources-folder/**") + .get("/core/resource/resolve") + .then() + .body( + containsString("sub-resources-folder/foo/bar/test-1.txt"), + containsString("sub-resources-folder/foo/bar/test-2.txt")); + + RestAssured.given() + .queryParam("path", "sub-resources-folder/foo/**") + .get("/core/resource/resolve") + .then() + .body( + containsString("sub-resources-folder/foo/bar/test-1.txt"), + containsString("sub-resources-folder/foo/bar/test-2.txt")); + + RestAssured.given() + .queryParam("path", "**/*.txt") + .get("/core/resource/resolve") + .then() + .body( + containsString("sub-resources-folder/foo/bar/test-1.txt"), + containsString("sub-resources-folder/foo/bar/test-2.txt")); + + RestAssured.given() + .queryParam("path", "sub-resources-folder/foo/bar/*.txt") + .get("/core/resource/resolve") + .then() + .body( + containsString("sub-resources-folder/foo/bar/test-1.txt"), + containsString("sub-resources-folder/foo/bar/test-2.txt")); + + // Resource that does not exist + RestAssured.given() + .queryParam("path", "sub-resources-folder/invalid") + .get("/core/resource/resolve") + .then() + .body(emptyOrNullString()); + } } diff --git a/integration-tests/main-devmode/pom.xml b/integration-tests/main-devmode/pom.xml index 603590e8fc..91827472de 100644 --- a/integration-tests/main-devmode/pom.xml +++ b/integration-tests/main-devmode/pom.xml @@ -43,6 +43,10 @@ <groupId>org.apache.camel.quarkus</groupId> <artifactId>camel-quarkus-log</artifactId> </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-rest</artifactId> + </dependency> <dependency> <groupId>org.apache.camel.quarkus</groupId> <artifactId>camel-quarkus-timer</artifactId> @@ -141,6 +145,19 @@ </exclusion> </exclusions> </dependency> + <dependency> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-rest-deployment</artifactId> + <version>${project.version}</version> + <type>pom</type> + <scope>test</scope> + <exclusions> + <exclusion> + <groupId>*</groupId> + <artifactId>*</artifactId> + </exclusion> + </exclusions> + </dependency> <dependency> <groupId>org.apache.camel.quarkus</groupId> <artifactId>camel-quarkus-timer-deployment</artifactId> diff --git a/integration-tests/main-devmode/src/test/java/org/apache/camel/quarkus/main/CamelMainRoutesIncludePatternWithDefaultLocationsDevModeTest.java b/integration-tests/main-devmode/src/test/java/org/apache/camel/quarkus/main/CamelMainRoutesIncludePatternWithDefaultLocationsDevModeTest.java new file mode 100644 index 0000000000..f516438058 --- /dev/null +++ b/integration-tests/main-devmode/src/test/java/org/apache/camel/quarkus/main/CamelMainRoutesIncludePatternWithDefaultLocationsDevModeTest.java @@ -0,0 +1,141 @@ +/* + * 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.quarkus.main; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; +import io.restassured.response.Response; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.Asset; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +public class CamelMainRoutesIncludePatternWithDefaultLocationsDevModeTest { + @RegisterExtension + static final QuarkusDevModeTest TEST = new QuarkusDevModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClass(CamelSupportResource.class) + .addAsResource(routeXml(), "camel/routes.xml") + .addAsResource(restsXml(), "camel-rest/rests.xml") + .addAsResource(routeTemplateXml(), "camel-template/templates.xml") + .addAsResource(applicationProperties(), "application.properties")); + + public static Asset routeXml() { + String xml = """ + <?xml version="1.0" encoding="UTF-8"?> + <routes> + <route id="route1-from-default-location"> + <from uri="direct:greeting"/> + <setBody><constant>Hello World</constant></setBody> + </route> + </routes>"""; + return new StringAsset(xml); + } + + public static Asset restsXml() { + String xml = """ + <rests xmlns="http://camel.apache.org/schema/spring"> + <rest id="rest1-from-default-location" path="/greeting"> + <get path="/hello"> + <to uri="direct:greeting"/> + </get> + </rest> + </rests>"""; + return new StringAsset(xml); + } + + public static Asset routeTemplateXml() { + String xml = """ + <routeTemplates> + <routeTemplate id="template1-from-default-location"> + <templateParameter name="name"/> + <templateParameter name="greeting"/> + <templateParameter name="myPeriod" defaultValue="3s"/> + <route> + <from uri="timer:{{name}}?period={{myPeriod}}"/> + <setBody><simple>{{greeting}} ${body}</simple></setBody> + <log message="${body}"/> + </route> + </routeTemplate> + </routeTemplates>"""; + return new StringAsset(xml); + } + + public static Asset applicationProperties() { + Writer writer = new StringWriter(); + + Properties props = new Properties(); + props.setProperty("quarkus.banner.enabled", "false"); + + try { + props.store(writer, ""); + } catch (IOException e) { + throw new RuntimeException(e); + } + + return new StringAsset(writer.toString()); + } + + @Test + public void testRoutesDiscoveryFromDefaultLocation() { + await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { + Response res = RestAssured.when().get("/test/describe").thenReturn(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body().jsonPath().getList("routes", String.class)).containsOnly("route1-from-default-location"); + assertThat(res.body().jsonPath().getList("rests", String.class)).containsOnly("rest1-from-default-location"); + assertThat(res.body().jsonPath().getList("templates", String.class)) + .containsOnly("template1-from-default-location"); + }); + + TEST.modifyResourceFile("camel/routes.xml", xml -> xml.replaceAll("route1", "route2")); + await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { + Response res = RestAssured.when().get("/test/describe").thenReturn(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body().jsonPath().getList("routes", String.class)).containsOnly("route2-from-default-location"); + }); + + TEST.modifyResourceFile("camel-rest/rests.xml", xml -> xml.replaceAll("rest1", "rest2")); + await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { + Response res = RestAssured.when().get("/test/describe").thenReturn(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body().jsonPath().getList("rests", String.class)).containsOnly("rest2-from-default-location"); + }); + + TEST.modifyResourceFile("camel-template/templates.xml", xml -> xml.replaceAll("template1", "template2")); + await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> { + Response res = RestAssured.when().get("/test/describe").thenReturn(); + + assertThat(res.statusCode()).isEqualTo(200); + assertThat(res.body().jsonPath().getList("templates", String.class)) + .containsOnly("template2-from-default-location"); + }); + } +} diff --git a/integration-tests/main-devmode/src/test/java/org/apache/camel/quarkus/main/CamelSupportResource.java b/integration-tests/main-devmode/src/test/java/org/apache/camel/quarkus/main/CamelSupportResource.java index 07aa03ec4b..8702cc89c0 100644 --- a/integration-tests/main-devmode/src/test/java/org/apache/camel/quarkus/main/CamelSupportResource.java +++ b/integration-tests/main-devmode/src/test/java/org/apache/camel/quarkus/main/CamelSupportResource.java @@ -26,6 +26,7 @@ import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import org.apache.camel.CamelContext; +import org.apache.camel.model.Model; @Path("/test") @ApplicationScoped @@ -38,10 +39,18 @@ public class CamelSupportResource { @Produces(MediaType.APPLICATION_JSON) public JsonObject describeMain() { JsonArrayBuilder routes = Json.createArrayBuilder(); + JsonArrayBuilder rests = Json.createArrayBuilder(); + JsonArrayBuilder templates = Json.createArrayBuilder(); context.getRoutes().forEach(route -> routes.add(route.getId())); + Model model = context.getCamelContextExtension().getContextPlugin(Model.class); + model.getRestDefinitions().forEach(rest -> rests.add(rest.getId())); + model.getRouteTemplateDefinitions().forEach(template -> templates.add(template.getId())); + return Json.createObjectBuilder() .add("routes", routes) + .add("rests", rests) + .add("templates", templates) .build(); } diff --git a/integration-tests/main-yaml/src/main/resources/application.properties b/integration-tests/main-yaml/src/main/resources/application.properties index 9abe188ba9..9c10a36dec 100644 --- a/integration-tests/main-yaml/src/main/resources/application.properties +++ b/integration-tests/main-yaml/src/main/resources/application.properties @@ -18,5 +18,4 @@ # # Main # -camel.main.routes-include-pattern = routes/my-routes.yaml,routes/my-rests.yaml,routes/my-templates.yaml camel.main.dump-routes = yaml diff --git a/integration-tests/main-yaml/src/main/resources/routes/my-rests.yaml b/integration-tests/main-yaml/src/main/resources/camel-rest/my-rests.yaml similarity index 100% rename from integration-tests/main-yaml/src/main/resources/routes/my-rests.yaml rename to integration-tests/main-yaml/src/main/resources/camel-rest/my-rests.yaml diff --git a/integration-tests/main-yaml/src/main/resources/routes/my-templates.yaml b/integration-tests/main-yaml/src/main/resources/camel-template/my-templates.yaml similarity index 100% rename from integration-tests/main-yaml/src/main/resources/routes/my-templates.yaml rename to integration-tests/main-yaml/src/main/resources/camel-template/my-templates.yaml diff --git a/integration-tests/main-yaml/src/main/resources/routes/my-routes.yaml b/integration-tests/main-yaml/src/main/resources/camel/my-routes.yaml similarity index 100% rename from integration-tests/main-yaml/src/main/resources/routes/my-routes.yaml rename to integration-tests/main-yaml/src/main/resources/camel/my-routes.yaml