diff --git a/catalog/pom.xml b/catalog/pom.xml index dfa5af7f4802..0e900a5ed11a 100644 --- a/catalog/pom.xml +++ b/catalog/pom.xml @@ -1813,19 +1813,6 @@ - - org.apache.camel.quarkus - camel-quarkus-integration-tests-support-custom-kamelet-resource - ${project.version} - pom - test - - - * - * - - - org.apache.camel.quarkus camel-quarkus-integration-tests-support-custom-log-component diff --git a/docs/modules/ROOT/pages/migration-guide/3.18.0.adoc b/docs/modules/ROOT/pages/migration-guide/3.18.0.adoc new file mode 100644 index 000000000000..5cfea2c27ee5 --- /dev/null +++ b/docs/modules/ROOT/pages/migration-guide/3.18.0.adoc @@ -0,0 +1,24 @@ += Camel Quarkus 3.18.0 Migration Guide + +The following guide outlines how to adapt your code to changes that were made in Camel Quarkus 3.18.0. + +== `camel-quarkus-yaml-dsl` is now required when using Kamelets with the Java DSL + +When using Kamelets in conjunction with Java DSL routes, you must ensure `camel-quarkus-yaml-dsl` is added to the project dependencies. + +[source,xml] +---- + + org.apache.camel.quarkus + camel-quarkus-yaml-dsl + +---- + +Failing to do this will result in a runtime exception like the following. + +[source,shell] +---- +java.lang.IllegalArgumentException: Cannot find RoutesBuilderLoader in classpath supporting file extension: kamelet.yaml +---- + +TIP: You can create Kamelets using Camel JBang, export them to a Camel Quarkus application and `camel-quarkus-yaml-dsl` will be added automatically. diff --git a/docs/modules/ROOT/pages/migration-guide/index.adoc b/docs/modules/ROOT/pages/migration-guide/index.adoc index e8a74ccdd437..afd30c3d918e 100644 --- a/docs/modules/ROOT/pages/migration-guide/index.adoc +++ b/docs/modules/ROOT/pages/migration-guide/index.adoc @@ -4,6 +4,7 @@ We do frequent releases, a release almost every month, and even though we strive Listed here are guides on how to migrate between major versions and anything of significance to watch for when upgrading from minor versions. +* xref:migration-guide/3.18.0.adoc[Camel Quarkus 3.17.x to Camel Quarkus 3.18.0 migration guide] * xref:migration-guide/3.17.0.adoc[Camel Quarkus 3.16.x to Camel Quarkus 3.17.0 migration guide] * xref:migration-guide/3.15.0.adoc[Camel Quarkus 3.14.x to Camel Quarkus 3.15.0 migration guide] * xref:migration-guide/3.2.0.adoc[Camel Quarkus 2.x to Camel Quarkus 3.2.0 migration guide] diff --git a/docs/modules/ROOT/pages/reference/extensions/kamelet.adoc b/docs/modules/ROOT/pages/reference/extensions/kamelet.adoc index c5cd4d3b21cf..a8e9f31efe9e 100644 --- a/docs/modules/ROOT/pages/reference/extensions/kamelet.adoc +++ b/docs/modules/ROOT/pages/reference/extensions/kamelet.adoc @@ -46,11 +46,6 @@ endif::[] [id="extensions-kamelet-usage"] == Usage -[id="extensions-kamelet-usage-preloading-kamelets-at-build-time"] -=== Preloading Kamelets at build-time - -This extension can preload a set of Kamelets at build time. You must include the names of each Kamelet, in the `quarkus.camel.kamelet.identifiers` configuration property. - [id="extensions-kamelet-usage-using-the-kamelet-catalog"] === Using the Kamelet Catalog @@ -67,7 +62,10 @@ Alternatively, you can add the `camel-kamelets` dependency to your application. ---- -You can select which Kamelets from the catalog you want to use by referencing their name in the `quarkus.camel.kamelet.identifiers` property. +[id="extensions-kamelet-usage-custom-kamelets"] +=== Custom Kamelets + +It's advised to name files containing your custom Kamelet definitions with the extension `.kamelet.yaml`. [id="extensions-kamelet-additional-camel-quarkus-configuration"] @@ -80,11 +78,14 @@ You can select which Kamelets from the catalog you want to use by referencing th |icon:lock[title=Fixed at build time] [[quarkus.camel.kamelet.identifiers]]`link:#quarkus.camel.kamelet.identifiers[quarkus.camel.kamelet.identifiers]` -List of kamelets identifiers to pre-load at build time. +Optional comma separated list of kamelet identifiers to configure for native mode support. +A kamelet identifier is the Kamelet file name without the .kamelet.yaml suffix.

-Each individual identifier is used to set the related {@link org.apache.camel.model.RouteTemplateDefinition} id. +The default value '*' will result in all discovered Kamelet definition files being included into the native image. +Note that this configuration option is only relevant when producing a native application. +

| List of `string` -| +| `*` |=== [.configuration-legend] diff --git a/extensions/kamelet/deployment/pom.xml b/extensions/kamelet/deployment/pom.xml index e3ddf4385eb5..ea98b7466234 100644 --- a/extensions/kamelet/deployment/pom.xml +++ b/extensions/kamelet/deployment/pom.xml @@ -38,10 +38,6 @@ org.apache.camel.quarkus camel-quarkus-kamelet - - org.apache.camel - camel-yaml-dsl - diff --git a/extensions/kamelet/deployment/src/main/java/org/apache/camel/quarkus/component/kamelet/deployment/KameletProcessor.java b/extensions/kamelet/deployment/src/main/java/org/apache/camel/quarkus/component/kamelet/deployment/KameletProcessor.java index 8588bdb1b174..10d4b773c0ea 100644 --- a/extensions/kamelet/deployment/src/main/java/org/apache/camel/quarkus/component/kamelet/deployment/KameletProcessor.java +++ b/extensions/kamelet/deployment/src/main/java/org/apache/camel/quarkus/component/kamelet/deployment/KameletProcessor.java @@ -16,38 +16,47 @@ */ package org.apache.camel.quarkus.component.kamelet.deployment; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; import java.util.HashSet; import java.util.List; -import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; +import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; -import io.quarkus.deployment.annotations.ExecutionTime; -import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageResourcePatternsBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; -import org.apache.camel.CamelContext; -import org.apache.camel.ExtendedCamelContext; -import org.apache.camel.Ordered; -import org.apache.camel.RoutesBuilder; -import org.apache.camel.builder.RouteBuilder; -import org.apache.camel.impl.DefaultCamelContext; -import org.apache.camel.model.RouteTemplateDefinition; -import org.apache.camel.quarkus.component.kamelet.EmptyKameletResource; +import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; +import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; +import io.quarkus.maven.dependency.ResolvedDependency; +import io.quarkus.paths.PathFilter; +import io.quarkus.paths.PathVisitor; import org.apache.camel.quarkus.component.kamelet.KameletConfiguration; -import org.apache.camel.quarkus.component.kamelet.KameletRecorder; -import org.apache.camel.quarkus.core.deployment.spi.CamelContextCustomizerBuildItem; -import org.apache.camel.spi.Resource; -import org.apache.camel.support.PluginHelper; +import org.apache.camel.util.FileUtil; +import org.apache.camel.util.ObjectHelper; +import org.apache.camel.util.StringHelper; +import org.eclipse.microprofile.config.ConfigProvider; import org.jboss.logging.Logger; class KameletProcessor { private static final Logger LOGGER = Logger.getLogger(KameletProcessor.class); + private static final String CLASS_PREFIX = "#class:"; + private static final String CLASSPATH_PREFIX = "classpath"; + private static final String FILE_PREFIX = "file"; + private static final String KAMELET_FILE_EXTENSION = ".kamelet.yaml"; private static final String FEATURE = "camel-kamelet"; @BuildStep @@ -55,115 +64,155 @@ FeatureBuildItem feature() { return new FeatureBuildItem(FEATURE); } - @BuildStep - KameletResolverBuildItem defaultResolver() { - return new KameletResolverBuildItem(new KameletResolver() { - @Override - public Optional resolve(String id, CamelContext context) throws Exception { - return Optional.ofNullable( - PluginHelper.getResourceLoader(context).resolveResource("/kamelets/" + id + ".kamelet.yaml")); + @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class) + void kameletNativeModeSupport( + CurateOutcomeBuildItem curateOutcome, + BuildProducer nativeImagePatterns, + BuildProducer reflectiveClass, + KameletConfiguration kameletConfig) { + + // The locations where kamelets are stored + List kameletLocations = ConfigProvider.getConfig() + .getOptionalValues("camel.component.kamelet.location", String.class) + .orElse(List.of("classpath:kamelets")); + + Set identifiers = kameletConfig.identifiers + .stream() + .map(String::trim) + .map(identifier -> identifier.replace(KAMELET_FILE_EXTENSION, "")) + .map(identifier -> identifier + KAMELET_FILE_EXTENSION) + .collect(Collectors.toUnmodifiableSet()); + + List kameletResources = new ArrayList<>(); + Set kameletClasspathPatterns = new HashSet<>(); + for (String kameletLocation : kameletLocations) { + String scheme = StringHelper.before(kameletLocation, ":"); + String location = StringHelper.after(kameletLocation, ":", kameletLocation); + if (ObjectHelper.isEmpty(scheme) || scheme.equals(CLASSPATH_PREFIX)) { + identifiers.forEach(identifier -> kameletClasspathPatterns + .add(FileUtil.stripLeadingSeparator(location) + "/" + identifier)); + } else if (scheme.equals(FILE_PREFIX)) { + boolean isDefaultIdentifier = identifiers.size() == 1 && identifiers.contains("*." + KAMELET_FILE_EXTENSION); + + Path kameletsDir = Paths.get(location); + if (Files.isDirectory(kameletsDir)) { + try { + Files.walkFileTree(kameletsDir, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + if (Files.isReadable(file)) { + String fileName = file.getFileName().toString(); + if ((isDefaultIdentifier && fileName.endsWith(KAMELET_FILE_EXTENSION)) + || (identifiers.contains(fileName))) { + kameletResources.add(FILE_PREFIX + ":" + file.toAbsolutePath()); + } + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException e) { + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + LOGGER.debugf(e, "Failed to walk kamelet directory %s", location); + } + } } - }); - } + } - @BuildStep - void loadResources( - List resolvers, - KameletConfiguration configuration, - BuildProducer resources) throws Exception { - - List kameletResolvers = resolvers.stream() - .map(KameletResolverBuildItem::getResolver) - .sorted(Comparator.comparingInt(Ordered::getOrder)) - .toList(); - - CamelContext context = new DefaultCamelContext(); - - for (String id : configuration.identifiers.orElse(Collections.emptyList())) { - for (KameletResolver resolver : kameletResolvers) { - resolver.resolve(id, context) - .map(r -> new KameletResourceBuildItem(id, r)) - .ifPresent(resources::produce); + if (!kameletClasspathPatterns.isEmpty()) { + nativeImagePatterns.produce(NativeImageResourcePatternsBuildItem.builder() + .includeGlobs(kameletClasspathPatterns) + .build()); + + PathFilter pathFilter = PathFilter.forIncludes(kameletClasspathPatterns); + PathVisitor pathVisitor = visit -> kameletResources.add(CLASSPATH_PREFIX + ":" + sanitizePath(visit.getPath())); + + // Discover Kamelets in the application artifact + ApplicationModel applicationModel = curateOutcome.getApplicationModel(); + ResolvedDependency appArtifact = applicationModel.getAppArtifact(); + appArtifact.getContentTree(pathFilter).walk(pathVisitor); + + // Discover Kamelets in other runtime dependencies + for (ResolvedDependency dependency : applicationModel.getRuntimeDependencies()) { + dependency.getContentTree(pathFilter).walk(pathVisitor); + } + } + + // Automatically register kamelet beans for reflection + if (!kameletResources.isEmpty()) { + Set kameletBeanClasses = resolveKameletBeanClasses(kameletResources); + if (!kameletBeanClasses.isEmpty()) { + reflectiveClass.produce(ReflectiveClassBuildItem.builder(kameletBeanClasses.toArray(new String[0])) + .fields() + .methods() + .build()); } } } - @Record(ExecutionTime.STATIC_INIT) - @BuildStep - CamelContextCustomizerBuildItem configureTemplates( - List resources, - BuildProducer reflectiveClass, - KameletRecorder recorder) throws Exception { + private Set resolveKameletBeanClasses(List kameletResources) { + Set kameletBeanClasses = new HashSet<>(); + for (String kameletResource : kameletResources) { + LOGGER.debugf("Processing kamelet resource %s", kameletResource); - List definitions = new ArrayList<>(); + try (InputStream stream = getKameletAsStream(kameletResource)) { + if (stream == null) { + continue; + } - try (CamelContext context = new DefaultCamelContext()) { - ExtendedCamelContext ecc = context.getCamelContextExtension(); + // Parse out any #class: references from the YAML. Avoids having to instantiate a CamelContext or YAML parser + try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) { + String line; - for (KameletResourceBuildItem item : resources) { - Resource resource = item.getResource(); - if (!resource.exists()) { - throw new IllegalStateException("Unable to load kamelet from: " + resource.getLocation()); - } + while ((line = reader.readLine()) != null) { + if (line.startsWith("#")) { + continue; + } - LOGGER.debugf("Loading kamelet from: %s", resource); - Collection rbs = PluginHelper.getRoutesLoader(ecc).findRoutesBuilders(resource); - for (RoutesBuilder rb : rbs) { - RouteBuilder routeBuilder = (RouteBuilder) rb; - routeBuilder.configure(); - if (routeBuilder.getRouteTemplateCollection().getRouteTemplates().isEmpty()) { - throw new IllegalStateException( - "No kamelet template was created for " - + "kamelet:" + item.getId() + ". It might be that the kamelet was malformed?"); - } else if (routeBuilder.getRouteTemplateCollection().getRouteTemplates().size() > 1) { - throw new IllegalStateException( - "A kamelet is not supposed to create more than one route (" - + "kamelet:" + item.getId() + "," - + "routes: " + routeBuilder.getRouteTemplateCollection().getRouteTemplates().size() - + ")"); - } + String beanClass = StringHelper.after(line, CLASS_PREFIX); + if (ObjectHelper.isNotEmpty(beanClass)) { + beanClass = StringHelper.removeQuotes(beanClass); - definitions.add(routeBuilder.getRouteTemplateCollection().getRouteTemplates().get(0)); - } - } - } + // Handle #class declarations that include constructor args + if (beanClass.contains("(")) { + beanClass = StringHelper.before(beanClass, "("); + } - // TODO: Improve / remove this https://github.com/apache/camel-quarkus/issues/5230 - // Use Quarkus recorder serialization friendly EmptyKameletResource instead of the default Resource. - // The resource will get reevaluated at runtime and replaced if it exists - definitions.forEach(definition -> { - Resource originalResource = definition.getResource(); - EmptyKameletResource resource = new EmptyKameletResource(); - resource.setScheme(originalResource.getScheme()); - resource.setLocation(originalResource.getLocation()); - resource.setExists(originalResource.exists()); - definition.setResource(resource); - //remove references to camelContext https://github.com/apache/camel-quarkus/issues/5849 - definition.setCamelContext(null); - if (definition.getRoute() != null && definition.getRoute().getOutputs() != null) { - definition.getRoute().getOutputs().forEach(o -> o.setCamelContext(null)); - } + // Ignore #class declarations with property placeholders + if (beanClass.contains("{{")) { + continue; + } - if (definition.getTemplateBeans() != null) { - Set beanClassNames = new HashSet<>(); - definition.getTemplateBeans().forEach(bean -> { - bean.setResource(resource); + LOGGER.debugf("Discovered kamelet bean class %s", beanClass); - String beanType = bean.getType(); - if (beanType != null && beanType.startsWith("#class:")) { - String className = beanType.substring("#class:".length()); - beanClassNames.add(className); + kameletBeanClasses.add(beanClass); + } } - }); - - reflectiveClass.produce(ReflectiveClassBuildItem.builder(beanClassNames.toArray(new String[0])) - .fields() - .methods() - .build()); + } + } catch (IOException e) { + LOGGER.debugf(e, "Failed processing kamelet resource %s", kameletResource); } - }); + } + return kameletBeanClasses; + } + + private InputStream getKameletAsStream(String kameletResource) throws IOException { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + String scheme = StringHelper.before(kameletResource, ":"); + String location = StringHelper.after(kameletResource, ":", kameletResource); + if (ObjectHelper.isEmpty(scheme) || scheme.equals(CLASSPATH_PREFIX)) { + return classLoader.getResourceAsStream(location); + } else if (scheme.equals(FILE_PREFIX)) { + return Files.newInputStream(Paths.get(location)); + } + return null; + } - return new CamelContextCustomizerBuildItem( - recorder.createTemplateLoaderCustomizer(definitions)); + private String sanitizePath(Path path) { + String normalizedPath = FileUtil.normalizePath(path.toString()); + return FileUtil.stripLeadingSeparator(normalizedPath); } } diff --git a/extensions/kamelet/deployment/src/main/java/org/apache/camel/quarkus/component/kamelet/deployment/KameletResolverBuildItem.java b/extensions/kamelet/deployment/src/main/java/org/apache/camel/quarkus/component/kamelet/deployment/KameletResolverBuildItem.java deleted file mode 100644 index f463f0bbf8a5..000000000000 --- a/extensions/kamelet/deployment/src/main/java/org/apache/camel/quarkus/component/kamelet/deployment/KameletResolverBuildItem.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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.component.kamelet.deployment; - -import io.quarkus.builder.item.MultiBuildItem; - -/** - * Build item used by kamelet providers to plug their own way of resolving kamelets giving a name. This could be - * leveraged by a future camel-quarkus-kamelet-catalog extension to resolve kamelets as they may have a different naming - * structure or location in the classpath. - */ -public final class KameletResolverBuildItem extends MultiBuildItem { - private final KameletResolver resolver; - - public KameletResolverBuildItem(KameletResolver resolver) { - this.resolver = resolver; - } - - public KameletResolver getResolver() { - return this.resolver; - } -} diff --git a/extensions/kamelet/deployment/src/main/java/org/apache/camel/quarkus/component/kamelet/deployment/KameletResourceBuildItem.java b/extensions/kamelet/deployment/src/main/java/org/apache/camel/quarkus/component/kamelet/deployment/KameletResourceBuildItem.java deleted file mode 100644 index 3ba3d7c8e73a..000000000000 --- a/extensions/kamelet/deployment/src/main/java/org/apache/camel/quarkus/component/kamelet/deployment/KameletResourceBuildItem.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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.component.kamelet.deployment; - -import io.quarkus.builder.item.MultiBuildItem; -import org.apache.camel.spi.Resource; - -/** - * Build item used by application developer to provide their own kamelets resources. This could be leveraged by - * application that have a custom mechanism to load and discovery kamelets. - */ -public final class KameletResourceBuildItem extends MultiBuildItem { - private final String id; - private final Resource resource; - - public KameletResourceBuildItem(String id, Resource resource) { - this.id = id; - this.resource = resource; - } - - public String getId() { - return id; - } - - public Resource getResource() { - return resource; - } -} diff --git a/extensions/kamelet/runtime/src/main/doc/usage.adoc b/extensions/kamelet/runtime/src/main/doc/usage.adoc index 56cbd3485967..56d981882a75 100644 --- a/extensions/kamelet/runtime/src/main/doc/usage.adoc +++ b/extensions/kamelet/runtime/src/main/doc/usage.adoc @@ -1,7 +1,3 @@ -=== Preloading Kamelets at build-time - -This extension can preload a set of Kamelets at build time. You must include the names of each Kamelet, in the `quarkus.camel.kamelet.identifiers` configuration property. - === Using the Kamelet Catalog A set of pre-made Kamelets can be found in the Kamelet Catalog. @@ -17,4 +13,6 @@ Alternatively, you can add the `camel-kamelets` dependency to your application. ---- -You can select which Kamelets from the catalog you want to use by referencing their name in the `quarkus.camel.kamelet.identifiers` property. +=== Custom Kamelets + +It's advised to name files containing your custom Kamelet definitions with the extension `.kamelet.yaml`. diff --git a/extensions/kamelet/runtime/src/main/java/org/apache/camel/quarkus/component/kamelet/EmptyKameletResource.java b/extensions/kamelet/runtime/src/main/java/org/apache/camel/quarkus/component/kamelet/EmptyKameletResource.java deleted file mode 100644 index aa17e84eadba..000000000000 --- a/extensions/kamelet/runtime/src/main/java/org/apache/camel/quarkus/component/kamelet/EmptyKameletResource.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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.component.kamelet; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Objects; - -import org.apache.camel.spi.Resource; - -/** - * TODO: Improve / remove this https://github.com/apache/camel-quarkus/issues/5230 - * Quarkus build time & serialization friendly implementation for Kamelet resources. This gets replaced at runtime - * when the resource is reevaluated. - */ -public final class EmptyKameletResource implements Resource { - private String scheme; - private String location; - private boolean exists; - - @Override - public String getScheme() { - return scheme; - } - - public void setScheme(String scheme) { - this.scheme = scheme; - } - - @Override - public String getLocation() { - return this.location; - } - - public void setLocation(String location) { - this.location = location; - } - - @Override - public boolean exists() { - return exists; - } - - public void setExists(boolean exists) { - this.exists = exists; - } - - @Override - public InputStream getInputStream() throws IOException { - return InputStream.nullInputStream(); - } - - @Override - public String toString() { - String prefix = scheme + ":"; - if (location.startsWith(prefix)) { - return "Resource[" + location + "]"; - } else { - return "Resource[" + prefix + location + "]"; - } - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Resource that = (Resource) o; - return scheme.equals(that.getScheme()) && location.equals(that.getLocation()); - } - - @Override - public int hashCode() { - return Objects.hash(scheme, location); - } -} diff --git a/extensions/kamelet/runtime/src/main/java/org/apache/camel/quarkus/component/kamelet/KameletConfiguration.java b/extensions/kamelet/runtime/src/main/java/org/apache/camel/quarkus/component/kamelet/KameletConfiguration.java index 58a1eb8a4221..4a4e4331073f 100644 --- a/extensions/kamelet/runtime/src/main/java/org/apache/camel/quarkus/component/kamelet/KameletConfiguration.java +++ b/extensions/kamelet/runtime/src/main/java/org/apache/camel/quarkus/component/kamelet/KameletConfiguration.java @@ -17,7 +17,6 @@ package org.apache.camel.quarkus.component.kamelet; import java.util.List; -import java.util.Optional; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; @@ -26,10 +25,13 @@ @ConfigRoot(name = "camel.kamelet", phase = ConfigPhase.BUILD_TIME) public class KameletConfiguration { /** - * List of kamelets identifiers to pre-load at build time. + * Optional comma separated list of kamelet identifiers to configure for native mode support. + * A kamelet identifier is the Kamelet file name without the .kamelet.yaml suffix. *

- * Each individual identifier is used to set the related {@link org.apache.camel.model.RouteTemplateDefinition} id. + * The default value '*' will result in all discovered Kamelet definition files being included into the native image. + * Note that this configuration option is only relevant when producing a native application. + *

*/ - @ConfigItem - public Optional> identifiers; + @ConfigItem(defaultValue = "*") + public List identifiers; } diff --git a/extensions/kamelet/runtime/src/main/java/org/apache/camel/quarkus/component/kamelet/KameletRecorder.java b/extensions/kamelet/runtime/src/main/java/org/apache/camel/quarkus/component/kamelet/KameletRecorder.java deleted file mode 100644 index 2763ca5a5851..000000000000 --- a/extensions/kamelet/runtime/src/main/java/org/apache/camel/quarkus/component/kamelet/KameletRecorder.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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.component.kamelet; - -import java.util.List; - -import io.quarkus.runtime.RuntimeValue; -import io.quarkus.runtime.annotations.Recorder; -import io.quarkus.runtime.annotations.RelaxedValidation; -import org.apache.camel.CamelContext; -import org.apache.camel.model.Model; -import org.apache.camel.model.RouteTemplateDefinition; -import org.apache.camel.spi.CamelContextCustomizer; -import org.apache.camel.spi.Resource; -import org.apache.camel.spi.ResourceLoader; -import org.apache.camel.support.PluginHelper; -import org.apache.camel.util.ObjectHelper; -import org.jboss.logging.Logger; - -@Recorder -public class KameletRecorder { - private static final Logger LOG = Logger.getLogger(KameletRecorder.class); - - public RuntimeValue createTemplateLoaderCustomizer( - @RelaxedValidation List definitions) { - - return new RuntimeValue<>(new CamelContextCustomizer() { - @Override - public void configure(CamelContext context) { - try { - // TODO: Improve / remove this: https://github.com/apache/camel-quarkus/issues/5230 - ResourceLoader resourceLoader = PluginHelper.getResourceLoader(context); - for (RouteTemplateDefinition definition : definitions) { - Resource originalResource = definition.getResource(); - String location = originalResource.getLocation(); - if (originalResource instanceof EmptyKameletResource && !ObjectHelper.isNotEmpty(location)) { - Resource resource = resourceLoader.resolveResource(location); - if (resource != null) { - definition.setResource(resource); - } else { - if (LOG.isDebugEnabled()) { - LOG.debugf("Failed resolving Kamelet resource %s. Resource dumping will be disabled.", - definition.getId()); - } - } - } - //return references to camelContext https://github.com/apache/camel-quarkus/issues/5849 - definition.setCamelContext(context); - if (definition.getRoute() != null && definition.getRoute().getOutputs() != null) { - definition.getRoute().getOutputs().forEach(o -> o.setCamelContext(context)); - } - - if (definition.getTemplateBeans() != null) { - definition.getTemplateBeans() - .stream() - .filter(bean -> bean.getResource() instanceof EmptyKameletResource) - .forEach(bean -> bean.setResource(resourceLoader.resolveResource(location))); - } - } - context.getCamelContextExtension().getContextPlugin(Model.class).addRouteTemplateDefinitions(definitions); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - }); - } -} diff --git a/integration-tests-support/custom-kamelet-resource/deployment/pom.xml b/integration-tests-support/custom-kamelet-resource/deployment/pom.xml deleted file mode 100644 index 842742c66b45..000000000000 --- a/integration-tests-support/custom-kamelet-resource/deployment/pom.xml +++ /dev/null @@ -1,78 +0,0 @@ - - - - - org.apache.camel.quarkus - camel-quarkus-integration-tests-support-custom-kamelet-resource-parent - 3.18.0-SNAPSHOT - ../pom.xml - - 4.0.0 - - camel-quarkus-integration-tests-support-custom-kamelet-resource-deployment - Camel Quarkus :: Integration Tests :: Support :: Custom Kamelet Resource :: Deployment - A test extension - - - - - org.apache.camel.quarkus - camel-quarkus-bom-test - ${project.version} - pom - import - - - - - - - org.apache.camel.quarkus - camel-quarkus-core-deployment - - - org.apache.camel.quarkus - camel-quarkus-kamelet-deployment - - - org.apache.camel.quarkus - camel-quarkus-integration-tests-support-custom-kamelet-resource - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - - - io.quarkus - quarkus-extension-processor - ${quarkus.version} - - - - - - - - diff --git a/integration-tests-support/custom-kamelet-resource/deployment/src/main/java/org/apache/camel/quarkus/it/support/kamelet/deployment/CustomKameletResourceProcessor.java b/integration-tests-support/custom-kamelet-resource/deployment/src/main/java/org/apache/camel/quarkus/it/support/kamelet/deployment/CustomKameletResourceProcessor.java deleted file mode 100644 index f7ef7690f3a4..000000000000 --- a/integration-tests-support/custom-kamelet-resource/deployment/src/main/java/org/apache/camel/quarkus/it/support/kamelet/deployment/CustomKameletResourceProcessor.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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.it.support.kamelet.deployment; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; - -import io.quarkus.deployment.annotations.BuildStep; -import org.apache.camel.quarkus.component.kamelet.deployment.KameletResourceBuildItem; -import org.apache.camel.support.ResourceSupport; - -public class CustomKameletResourceProcessor { - private static final String KAMELET_ID = "custom-log"; - private static final String KAMELET_RES_SCHEME = "app"; - private static final String KAMELET_RES_LOCATION = "custom-log.kamelet.yaml"; - - private static final String KAMELET = """ - apiVersion: camel.apache.org/v1alpha1 - kind: Kamelet - metadata: - name: custom-log - labels: - camel.apache.org/kamelet.type: "sink" - camel.apache.org/kamelet.name: "custom-kamelet-resource" - camel.apache.org/kamelet.version: "v1alpha1" - spec: - definition: - title: "Logger" - description: "Logger" - properties: - loggerName: - title: Name of the logging category - description: Name of the logging category - type: string - default: "logger" - showAll: - title: Show All - description: Show All - type: boolean - default: false - multiLine: - title: Multi Line - description: Multi Line - type: boolean - default: false - dependencies: - - "camel:log" - template: - from: - uri: "kamelet:source" - steps: - - to: - uri: "log" - parameters: - loggerName: "{{loggerName}}" - showAll: "{{showAll}}" - multiline: "{{multiLine}}" - - """; - - @BuildStep - KameletResourceBuildItem resource() { - return new KameletResourceBuildItem(KAMELET_ID, new ResourceSupport(KAMELET_RES_SCHEME, KAMELET_RES_LOCATION) { - @Override - public boolean exists() { - return true; - } - - @Override - public InputStream getInputStream() throws IOException { - return new ByteArrayInputStream(KAMELET.getBytes(StandardCharsets.UTF_8)); - } - }); - } -} diff --git a/integration-tests-support/custom-kamelet-resource/pom.xml b/integration-tests-support/custom-kamelet-resource/pom.xml index b2bbfc7119e7..c002aececc90 100644 --- a/integration-tests-support/custom-kamelet-resource/pom.xml +++ b/integration-tests-support/custom-kamelet-resource/pom.xml @@ -26,14 +26,8 @@ ../pom.xml - camel-quarkus-integration-tests-support-custom-kamelet-resource-parent - pom + camel-quarkus-integration-tests-support-custom-kamelet-resource Camel Quarkus :: Integration Tests :: Support :: Custom Kamelet Resource - - runtime - deployment - - diff --git a/integration-tests-support/custom-kamelet-resource/runtime/pom.xml b/integration-tests-support/custom-kamelet-resource/runtime/pom.xml deleted file mode 100644 index 881c5a11b599..000000000000 --- a/integration-tests-support/custom-kamelet-resource/runtime/pom.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - org.apache.camel.quarkus - camel-quarkus-integration-tests-support-custom-kamelet-resource-parent - 3.18.0-SNAPSHOT - ../pom.xml - - 4.0.0 - - camel-quarkus-integration-tests-support-custom-kamelet-resource - Camel Quarkus :: Integration Tests :: Support :: Custom Kamelet Resource :: Runtime - A test extension - - - - org.apache.camel.quarkus - camel-quarkus-core - - - org.apache.camel.quarkus - camel-quarkus-kamelet - - - - - - - io.quarkus - quarkus-extension-maven-plugin - - - maven-compiler-plugin - - - - io.quarkus - quarkus-extension-processor - ${quarkus.version} - - - - - - - diff --git a/extensions/kamelet/deployment/src/main/java/org/apache/camel/quarkus/component/kamelet/deployment/KameletResolver.java b/integration-tests-support/custom-kamelet-resource/src/main/java/org/apache/camel/quarkus/test/support/kamelet/AdditionBean.java similarity index 66% rename from extensions/kamelet/deployment/src/main/java/org/apache/camel/quarkus/component/kamelet/deployment/KameletResolver.java rename to integration-tests-support/custom-kamelet-resource/src/main/java/org/apache/camel/quarkus/test/support/kamelet/AdditionBean.java index a32305f72614..adfe48fd3c2b 100644 --- a/extensions/kamelet/deployment/src/main/java/org/apache/camel/quarkus/component/kamelet/deployment/KameletResolver.java +++ b/integration-tests-support/custom-kamelet-resource/src/main/java/org/apache/camel/quarkus/test/support/kamelet/AdditionBean.java @@ -14,19 +14,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.camel.quarkus.component.kamelet.deployment; +package org.apache.camel.quarkus.test.support.kamelet; -import java.util.Optional; +public class AdditionBean { + private final int a; + private final int b; -import org.apache.camel.CamelContext; -import org.apache.camel.Ordered; -import org.apache.camel.spi.Resource; - -public interface KameletResolver extends Ordered { - Optional resolve(String id, CamelContext context) throws Exception; + public AdditionBean(int a, int b) { + this.a = a; + this.b = b; + } - @Override - default int getOrder() { - return Ordered.LOWEST; + public int getResult() { + return a + b; } } diff --git a/integration-tests/kamelet/src/main/resources/kamelets/logger.kamelet.yaml b/integration-tests-support/custom-kamelet-resource/src/main/resources/org/apache/camel/quarkus/custom/kamelet/custom.kamelet.yaml similarity index 55% rename from integration-tests/kamelet/src/main/resources/kamelets/logger.kamelet.yaml rename to integration-tests-support/custom-kamelet-resource/src/main/resources/org/apache/camel/quarkus/custom/kamelet/custom.kamelet.yaml index 7e822c9128d7..1885641b0a13 100644 --- a/integration-tests/kamelet/src/main/resources/kamelets/logger.kamelet.yaml +++ b/integration-tests-support/custom-kamelet-resource/src/main/resources/org/apache/camel/quarkus/custom/kamelet/custom.kamelet.yaml @@ -15,43 +15,36 @@ # limitations under the License. # -apiVersion: camel.apache.org/v1alpha1 +apiVersion: camel.apache.org/v1 kind: Kamelet metadata: - name: logger + name: custom labels: camel.apache.org/kamelet.type: "sink" - camel.apache.org/kamelet.name: "log" - camel.apache.org/kamelet.version: "v1alpha1" + camel.apache.org/kamelet.name: "custom-kamelet-resource" + camel.apache.org/kamelet.version: "v1" spec: definition: - title: "Logger" - description: "Logger" + title: "Number Adder" + description: "Adds two numbers together" properties: - loggerName: - title: Name of the logging category - description: Name of the logging category - type: string - default: "logger" - showAll: - title: Show All - description: Show All - type: boolean - default: false - multiLine: - title: Multi Line - description: Multi Line - type: boolean - default: false + numberA: + title: The first number + description: The first number + type: int + default: 1 + numberB: + title: The second number + description: The second number + type: int + default: 1 dependencies: - - "camel:log" + - "camel:bean" template: + beans: + - name: additionBean + type: "#class:org.apache.camel.quarkus.test.support.kamelet.AdditionBean({{numberA}}, {{numberB}})" from: uri: "kamelet:source" steps: - - to: - uri: "log" - parameters: - loggerName: "{{loggerName}}" - showAll: "{{showAll}}" - multiline: "{{multiLine}}" + - to: "bean:{{additionBean}}?method=getResult" diff --git a/integration-tests/kamelet/pom.xml b/integration-tests/kamelet/pom.xml index 99f7012568a9..a33e836e1d64 100644 --- a/integration-tests/kamelet/pom.xml +++ b/integration-tests/kamelet/pom.xml @@ -149,19 +149,6 @@ - - org.apache.camel.quarkus - camel-quarkus-integration-tests-support-custom-kamelet-resource-deployment - ${project.version} - pom - test - - - * - * - - - org.apache.camel.quarkus camel-quarkus-kamelet-deployment diff --git a/integration-tests/kamelet/src/main/java/org/apache/camel/quarkus/component/kamelet/it/KameletResource.java b/integration-tests/kamelet/src/main/java/org/apache/camel/quarkus/component/kamelet/it/KameletResource.java index b8bf3516faff..fc9c2dc1b97c 100644 --- a/integration-tests/kamelet/src/main/java/org/apache/camel/quarkus/component/kamelet/it/KameletResource.java +++ b/integration-tests/kamelet/src/main/java/org/apache/camel/quarkus/component/kamelet/it/KameletResource.java @@ -17,9 +17,6 @@ package org.apache.camel.quarkus.component.kamelet.it; import jakarta.inject.Inject; -import jakarta.json.Json; -import jakarta.json.JsonArray; -import jakarta.json.JsonArrayBuilder; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; @@ -30,8 +27,6 @@ import org.apache.camel.CamelContext; import org.apache.camel.ConsumerTemplate; import org.apache.camel.FluentProducerTemplate; -import org.apache.camel.model.Model; -import org.apache.camel.model.OptionalIdentifiedDefinition; @Path("/kamelet") public class KameletResource { @@ -81,21 +76,6 @@ public String invoke(@PathParam("name") String name, String message) throws Exce return fluentProducerTemplate.toF("kamelet:%s", name).withBody(message).request(String.class); } - @Path("/list") - @GET - @Produces(MediaType.APPLICATION_JSON) - public JsonArray list() { - JsonArrayBuilder builder = Json.createArrayBuilder(); - - camelContext.getCamelContextExtension().getContextPlugin(Model.class) - .getRouteTemplateDefinitions() - .stream() - .map(OptionalIdentifiedDefinition::getId) - .forEach(builder::add); - - return builder.build(); - } - @Path("/locationAtRuntime/{name}") @POST @Produces(MediaType.TEXT_PLAIN) @@ -116,4 +96,18 @@ public String greeting() { public String pipe() { return consumerTemplate.receiveBody("seda:greetingFromProperty", 10000, String.class); } + + @Path("/injector") + @GET + @Produces(MediaType.TEXT_PLAIN) + public String injector() { + return consumerTemplate.receiveBody("seda:injector", 10000, String.class); + } + + @Path("/custom") + @GET + @Produces(MediaType.TEXT_PLAIN) + public String custom() { + return fluentProducerTemplate.to("direct:custom").request(String.class); + } } diff --git a/integration-tests/kamelet/src/main/java/org/apache/camel/quarkus/component/kamelet/it/KameletRoutes.java b/integration-tests/kamelet/src/main/java/org/apache/camel/quarkus/component/kamelet/it/KameletRoutes.java index afb8520dac00..54eac55c649d 100644 --- a/integration-tests/kamelet/src/main/java/org/apache/camel/quarkus/component/kamelet/it/KameletRoutes.java +++ b/integration-tests/kamelet/src/main/java/org/apache/camel/quarkus/component/kamelet/it/KameletRoutes.java @@ -67,6 +67,12 @@ public void configure() throws Exception { from("kamelet:greeting") .to("seda:greeting"); + + from("kamelet:injector?delay=-1") + .to("seda:injector"); + + from("direct:custom") + .to("kamelet:custom?numberA=5&numberB=10"); } @RegisterForReflection @@ -76,8 +82,4 @@ public void process(Exchange exchange) { exchange.getMessage().setBody(exchange.getMessage().getBody(String.class) + "-suffix"); } } - - @RegisterForReflection(fields = false, targets = { String.class }) - public static class StringUpperCaseReflectionForUpperKamelet { - } } diff --git a/integration-tests/kamelet/src/main/resources/application.properties b/integration-tests/kamelet/src/main/resources/application.properties index 296f9d8bf1cb..73eb88d9b150 100644 --- a/integration-tests/kamelet/src/main/resources/application.properties +++ b/integration-tests/kamelet/src/main/resources/application.properties @@ -14,11 +14,8 @@ ## See the License for the specific language governing permissions and ## limitations under the License. ## --------------------------------------------------------------------------- +camel.component.kamelet.location=classpath:kamelets,classpath:org/apache/camel/quarkus/custom/kamelet camel.kamelet.setBodyFromProperties.bodyValueFromProperty=Camel Quarkus Kamelet Property camel.main.routes-include-pattern=pipes/greeting-pipe.yaml - -quarkus.camel.kamelet.identifiers = injector,logger,greeting,greeting-from-property - -# this is needed to actually test that kamelet are preloaded at build time: -camel.component.kamelet.location = file:/invalid +quarkus.camel.kamelet.identifiers = injector,logger,greeting,greeting-from-property,custom quarkus.native.resources.includes=kamelets-runtime/*.xml,pipes/*.yaml \ No newline at end of file diff --git a/integration-tests/kamelet/src/main/resources/kamelets/injector.kamelet.yaml b/integration-tests/kamelet/src/main/resources/kamelets/injector.kamelet.yaml index 6cfaf7bfe578..bca5d7e176b9 100644 --- a/integration-tests/kamelet/src/main/resources/kamelets/injector.kamelet.yaml +++ b/integration-tests/kamelet/src/main/resources/kamelets/injector.kamelet.yaml @@ -15,14 +15,14 @@ # limitations under the License. # -apiVersion: camel.apache.org/v1alpha1 +apiVersion: camel.apache.org/v1 kind: Kamelet metadata: name: injector labels: camel.apache.org/kamelet.type: "source" camel.apache.org/kamelet.name: "injector" - camel.apache.org/kamelet.version: "v1alpha1" + camel.apache.org/kamelet.version: "v1" camel.apache.org/kamelet.revision: "1" spec: definition: @@ -34,6 +34,11 @@ spec: description: Delay type: string default: "1s" + repeatCount: + title: Repeat Count + description: Repeat Count + type: string + default: "1" types: out: mediaType: application/json @@ -44,8 +49,8 @@ spec: uri: timer parameters: timerName: "{{routeId}}" - period: "{{delay}}" + delay: "{{delay}}" + repeatCount: "{{repeatCount}}" steps: - setBody: - exchangeProperty: "CamelTimerCounter" - - to: "kamelet:sink" + constant: "Hello World!" diff --git a/integration-tests/kamelet/src/test/java/org/apache/camel/quarkus/component/kamelet/it/KameletTest.java b/integration-tests/kamelet/src/test/java/org/apache/camel/quarkus/component/kamelet/it/KameletTest.java index 8eedef0f8d82..d24f44402826 100644 --- a/integration-tests/kamelet/src/test/java/org/apache/camel/quarkus/component/kamelet/it/KameletTest.java +++ b/integration-tests/kamelet/src/test/java/org/apache/camel/quarkus/component/kamelet/it/KameletTest.java @@ -16,16 +16,12 @@ */ package org.apache.camel.quarkus.component.kamelet.it; -import java.util.ArrayList; - import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; import io.restassured.http.ContentType; -import io.restassured.response.Response; import org.junit.jupiter.api.Test; import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertTrue; @QuarkusTest class KameletTest { @@ -89,23 +85,6 @@ public void testInvoke() { .body(is("Kamelet2-suffix")); } - @Test - public void testDiscovered() { - Response resp = RestAssured.given() - .contentType(ContentType.JSON) - .when() - .get("/kamelet/list"); - resp.then().statusCode(200); - - ArrayList jsonAsArrayList = resp.body() - .jsonPath().get(""); - - assertTrue(jsonAsArrayList.contains("injector")); - assertTrue(jsonAsArrayList.contains("logger")); - assertTrue(jsonAsArrayList.contains("custom-log")); - assertTrue(jsonAsArrayList.contains("greeting")); - } - @Test public void testKameletLocationAtRuntime() { RestAssured.given() @@ -130,4 +109,20 @@ public void pipe() { .statusCode(200) .body(is("Hello Pipe")); } + + @Test + public void testInjectorKamelet() { + RestAssured.get("/kamelet/injector") + .then() + .statusCode(200) + .body(is("Hello World!")); + } + + @Test + public void testKameletFromDependency() { + RestAssured.get("/kamelet/custom") + .then() + .statusCode(200) + .body(is("15")); + } }