This is an automated email from the ASF dual-hosted git repository. github-bot pushed a commit to branch camel-main in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git
commit d1901e753a9411ea0f1de450e9cbe683c5ff275b Author: Nicolas Filotto <nfilo...@talend.com> AuthorDate: Wed Mar 8 10:28:33 2023 +0100 Ref #4384: Groovy DSL - Add support of Groovy extensions --- .../pages/reference/extensions/groovy-dsl.adoc | 6 - .../dsl/groovy/deployment/GroovyDslProcessor.java | 65 +++++++- .../runtime/src/main/doc/limitations.adoc | 1 - integration-tests/groovy-dsl/pom.xml | 54 +++++- .../quarkus/dsl/groovy/GroovyDslResource.java | 6 + .../main/resources/routes/routes-with-eip.groovy | 36 ++++ .../camel/quarkus/dsl/groovy/GroovyDslTest.java | 181 +++++++++++++++------ 7 files changed, 285 insertions(+), 64 deletions(-) diff --git a/docs/modules/ROOT/pages/reference/extensions/groovy-dsl.adoc b/docs/modules/ROOT/pages/reference/extensions/groovy-dsl.adoc index f9c62daa0d..1926f73527 100644 --- a/docs/modules/ROOT/pages/reference/extensions/groovy-dsl.adoc +++ b/docs/modules/ROOT/pages/reference/extensions/groovy-dsl.adoc @@ -43,9 +43,3 @@ Or add the coordinates to your existing project: ifeval::[{doc-show-user-guide-link} == true] Check the xref:user-guide/index.adoc[User guide] for more information about writing Camel Quarkus applications. endif::[] - -[id="extensions-groovy-dsl-camel-quarkus-limitations"] -== Camel Quarkus limitations - -The Groovy extensions are not supported which means that the extensions defined in the Camel project are ignored, more details in https://github.com/apache/camel-quarkus/issues/4384[Groovy DSL - Add support of Groovy extensions issue #4384]. - diff --git a/extensions/groovy-dsl/deployment/src/main/java/org/apache/camel/quarkus/dsl/groovy/deployment/GroovyDslProcessor.java b/extensions/groovy-dsl/deployment/src/main/java/org/apache/camel/quarkus/dsl/groovy/deployment/GroovyDslProcessor.java index d39b5eb8fc..6a8fd78dfd 100644 --- a/extensions/groovy-dsl/deployment/src/main/java/org/apache/camel/quarkus/dsl/groovy/deployment/GroovyDslProcessor.java +++ b/extensions/groovy-dsl/deployment/src/main/java/org/apache/camel/quarkus/dsl/groovy/deployment/GroovyDslProcessor.java @@ -19,14 +19,19 @@ package org.apache.camel.quarkus.dsl.groovy.deployment; import java.io.IOException; import java.io.InputStream; +import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.GeneratedClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; @@ -34,6 +39,10 @@ import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; import io.quarkus.deployment.pkg.steps.NativeBuild; import io.quarkus.maven.dependency.ResolvedDependency; import io.quarkus.paths.PathCollection; +import org.apache.camel.CamelContext; +import org.apache.camel.Exchange; +import org.apache.camel.ExchangePattern; +import org.apache.camel.Message; import org.apache.camel.quarkus.core.deployment.main.CamelMainHelper; import org.apache.camel.quarkus.dsl.groovy.runtime.Configurer; import org.apache.camel.quarkus.support.dsl.deployment.DslGeneratedClassBuildItem; @@ -44,6 +53,9 @@ import org.codehaus.groovy.control.CompilationUnit; import org.codehaus.groovy.control.CompilerConfiguration; import org.codehaus.groovy.control.Phases; import org.codehaus.groovy.tools.GroovyClass; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.IndexView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -53,6 +65,11 @@ import static org.apache.camel.quarkus.support.dsl.deployment.DslSupportProcesso public class GroovyDslProcessor { private static final Logger LOG = LoggerFactory.getLogger(GroovyDslProcessor.class); + private static final List<Class<?>> CAMEL_REFLECTIVE_CLASSES = Arrays.asList( + Exchange.class, + Message.class, + ExchangePattern.class, + CamelContext.class); private static final String PACKAGE_NAME = "org.apache.camel.quarkus.dsl.groovy.generated"; private static final String FILE_FORMAT = """ package %s @@ -117,12 +134,48 @@ public class GroovyDslProcessor { } } - // To put it back once the Groovy extensions will be supported (https://github.com/apache/camel-quarkus/issues/4384) - // @BuildStep - // void registerNativeImageResources(BuildProducer<ServiceProviderBuildItem> serviceProvider) { - // serviceProvider - // .produce(ServiceProviderBuildItem.allProvidersFromClassPath("org.codehaus.groovy.runtime.ExtensionModule")); - // } + @BuildStep(onlyIf = NativeBuild.class) + void registerReflectiveClasses( + BuildProducer<ReflectiveClassBuildItem> reflectiveClass, + CombinedIndexBuildItem combinedIndexBuildItem) { + + IndexView view = combinedIndexBuildItem.getIndex(); + + for (Class<?> type : CAMEL_REFLECTIVE_CLASSES) { + DotName name = DotName.createSimple(type.getName()); + + if (type.isInterface()) { + for (ClassInfo info : view.getAllKnownImplementors(name)) { + reflectiveClass.produce(ReflectiveClassBuildItem.builder(info.name().toString()).methods().build()); + } + } else { + for (ClassInfo info : view.getAllKnownSubclasses(name)) { + reflectiveClass.produce(ReflectiveClassBuildItem.builder(info.name().toString()).methods().build()); + } + } + + reflectiveClass.produce(ReflectiveClassBuildItem.builder(type).methods().fields(type.isEnum()).build()); + } + + Set<Class<?>> types = new HashSet<>(); + // Register all the Camel return types of public methods of the camel reflective classes for reflection to + // be accessible in native mode from a Groovy resource + for (Class<?> c : CAMEL_REFLECTIVE_CLASSES) { + for (Method method : c.getMethods()) { + if (!method.getDeclaringClass().equals(Object.class)) { + Class<?> returnType = method.getReturnType(); + if (returnType.getPackageName().startsWith("org.apache.camel.") + && !CAMEL_REFLECTIVE_CLASSES.contains(returnType)) { + types.add(returnType); + } + } + } + } + // Allow access to methods by reflection to be accessible in native mode from a Groovy resource + reflectiveClass.produce( + ReflectiveClassBuildItem.builder(types.toArray(new Class<?>[0])).constructors(false).methods().build()); + + } /** * Convert a Groovy script into a Groovy class to be able to compile it. diff --git a/extensions/groovy-dsl/runtime/src/main/doc/limitations.adoc b/extensions/groovy-dsl/runtime/src/main/doc/limitations.adoc deleted file mode 100644 index a77743a491..0000000000 --- a/extensions/groovy-dsl/runtime/src/main/doc/limitations.adoc +++ /dev/null @@ -1 +0,0 @@ -The Groovy extensions are not supported which means that the extensions defined in the Camel project are ignored, more details in https://github.com/apache/camel-quarkus/issues/4384[Groovy DSL - Add support of Groovy extensions issue #4384]. diff --git a/integration-tests/groovy-dsl/pom.xml b/integration-tests/groovy-dsl/pom.xml index 07f11a865a..2e1d62d904 100644 --- a/integration-tests/groovy-dsl/pom.xml +++ b/integration-tests/groovy-dsl/pom.xml @@ -30,6 +30,9 @@ <name>Camel Quarkus :: Integration Tests :: Groovy DSL</name> <description>Integration tests for Camel Groovy DSL extension</description> + <properties> + <quarkus.runner>${project.build.directory}/quarkus-app/quarkus-run.jar</quarkus.runner> + </properties> <dependencies> <dependency> <groupId>org.apache.camel.quarkus</groupId> @@ -67,13 +70,18 @@ <scope>test</scope> </dependency> <dependency> - <groupId>io.rest-assured</groupId> - <artifactId>rest-assured</artifactId> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> <scope>test</scope> </dependency> <dependency> - <groupId>org.assertj</groupId> - <artifactId>assertj-core</artifactId> + <groupId>org.apache.camel.quarkus</groupId> + <artifactId>camel-quarkus-integration-tests-process-executor-support</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.awaitility</groupId> + <artifactId>awaitility</artifactId> <scope>test</scope> </dependency> </dependencies> @@ -177,6 +185,7 @@ </activation> <properties> <quarkus.package.type>native</quarkus.package.type> + <quarkus.runner>${project.build.directory}/${project.artifactId}-${project.version}-runner</quarkus.runner> </properties> <build> <plugins> @@ -191,6 +200,43 @@ </goals> </execution> </executions> + <configuration> + <systemProperties> + <quarkus.runner>${quarkus.runner}</quarkus.runner> + </systemProperties> + </configuration> + </plugin> + </plugins> + </build> + </profile> + <profile> + <id>full</id> + <activation> + <property> + <name>!quickly</name> + </property> + </activation> + <build> + <plugins> + <plugin> + <!-- Move surefire:test to integration-test phase to be able to run + java -jar target/*runner.jar from a test --> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <executions> + <execution> + <id>default-test</id> + <goals> + <goal>test</goal> + </goals> + <phase>integration-test</phase> + <configuration> + <systemProperties> + <quarkus.runner>${quarkus.runner}</quarkus.runner> + </systemProperties> + </configuration> + </execution> + </executions> </plugin> </plugins> </build> diff --git a/integration-tests/groovy-dsl/src/main/java/org/apache/camel/quarkus/dsl/groovy/GroovyDslResource.java b/integration-tests/groovy-dsl/src/main/java/org/apache/camel/quarkus/dsl/groovy/GroovyDslResource.java index 5d0585c253..8eb3c028b2 100644 --- a/integration-tests/groovy-dsl/src/main/java/org/apache/camel/quarkus/dsl/groovy/GroovyDslResource.java +++ b/integration-tests/groovy-dsl/src/main/java/org/apache/camel/quarkus/dsl/groovy/GroovyDslResource.java @@ -44,6 +44,12 @@ public class GroovyDslResource { @Inject ProducerTemplate producerTemplate; + @GET + @Produces(MediaType.TEXT_PLAIN) + public String ready() { + return "OK"; + } + @Path("/main/groovyRoutesBuilderLoader") @GET @Produces(MediaType.TEXT_PLAIN) diff --git a/integration-tests/groovy-dsl/src/main/resources/routes/routes-with-eip.groovy b/integration-tests/groovy-dsl/src/main/resources/routes/routes-with-eip.groovy new file mode 100644 index 0000000000..8d21881910 --- /dev/null +++ b/integration-tests/groovy-dsl/src/main/resources/routes/routes-with-eip.groovy @@ -0,0 +1,36 @@ +/* + * 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. + */ + +from('direct:routes-with-eip-setBody') + .id('routes-with-eip-setBody') + .setBody { "true" } + +from('direct:routes-with-eip-body') + .id('routes-with-eip-body') + .transform().body { b -> "true"} + +from('direct:routes-with-eip-message') + .id('routes-with-eip-message') + .transform().message { m -> m.body = "true"} + +from('direct:routes-with-eip-exchange') + .id('routes-with-eip-exchange') + .transform().exchange { e -> e.in.body = "true"} + +from('direct:routes-with-eip-process') + .id('routes-with-eip-process') + .process {e -> e.in.body = "true" } diff --git a/integration-tests/groovy-dsl/src/test/java/org/apache/camel/quarkus/dsl/groovy/GroovyDslTest.java b/integration-tests/groovy-dsl/src/test/java/org/apache/camel/quarkus/dsl/groovy/GroovyDslTest.java index fdc6a4faf4..82ce107208 100644 --- a/integration-tests/groovy-dsl/src/test/java/org/apache/camel/quarkus/dsl/groovy/GroovyDslTest.java +++ b/integration-tests/groovy-dsl/src/test/java/org/apache/camel/quarkus/dsl/groovy/GroovyDslTest.java @@ -16,64 +16,151 @@ */ package org.apache.camel.quarkus.dsl.groovy; -import io.quarkus.test.junit.QuarkusTest; -import io.restassured.RestAssured; +import java.util.concurrent.TimeUnit; + import org.apache.camel.dsl.groovy.GroovyRoutesBuilderLoader; -import org.hamcrest.CoreMatchers; +import org.apache.camel.quarkus.test.support.process.QuarkusProcessExecutor; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.util.EntityUtils; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.zeroturnaround.exec.StartedProcess; + +import static org.assertj.core.api.Assertions.assertThat; -@QuarkusTest class GroovyDslTest { + private static int port; + private static StartedProcess process; + + @BeforeAll + static void start() throws Exception { + // Need to use an external process to test the extension because of a CL issue that happens only on test mode + // due to the fact that groovy is defined as a parent first artifact + QuarkusProcessExecutor quarkusProcessExecutor = new QuarkusProcessExecutor(); + process = quarkusProcessExecutor.start(); + port = quarkusProcessExecutor.getHttpPort(); + awaitStartup(); + } + + @AfterAll + static void stop() { + if (process != null && process.getProcess().isAlive()) { + process.getProcess().destroy(); + } + } + + private static String toAbsolutePath(String relativePath) { + return String.format("http://localhost:%d/%s", port, relativePath); + } + + private static void awaitStartup() { + Awaitility.await().atMost(10, TimeUnit.SECONDS).pollDelay(1, TimeUnit.SECONDS).until(() -> { + HttpUriRequest request = new HttpGet(toAbsolutePath("/groovy-dsl")); + try (CloseableHttpClient client = HttpClientBuilder.create().build()) { + HttpResponse httpResponse = client.execute(request); + return httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK; + } catch (Exception e) { + return false; + } + }); + } + @Test - void groovyHello() { - RestAssured.given() - .body("John Smith") - .post("/groovy-dsl/hello") - .then() - .statusCode(200) - .body(CoreMatchers.is("Hello John Smith from Groovy!")); + void groovyHello() throws Exception { + try (CloseableHttpClient client = HttpClientBuilder.create().build()) { + // Given + HttpPost httpPost = new HttpPost(toAbsolutePath("/groovy-dsl/hello")); + httpPost.setEntity(new StringEntity("John Smith", ContentType.TEXT_PLAIN)); + + // When + HttpResponse httpResponse = client.execute(httpPost); + + // Then + assertThat(httpResponse.getStatusLine().getStatusCode()).isEqualTo(HttpStatus.SC_OK); + assertThat(EntityUtils.toString(httpResponse.getEntity())).isEqualTo("Hello John Smith from Groovy!"); + } } @Test - void testMainInstanceWithJavaRoutes() { - RestAssured.given() - .get("/groovy-dsl/main/groovyRoutesBuilderLoader") - .then() - .statusCode(200) - .body(CoreMatchers.is(GroovyRoutesBuilderLoader.class.getName())); - - RestAssured.given() - .get("/groovy-dsl/main/routeBuilders") - .then() - .statusCode(200) - .body(CoreMatchers.is("")); - - RestAssured.given() - .get("/groovy-dsl/main/routes") - .then() - .statusCode(200) - .body(CoreMatchers.is( - "my-groovy-route,routes-with-components-configuration,routes-with-dataformats-configuration,routes-with-endpoint-dsl,routes-with-error-handler,routes-with-languages-configuration,routes-with-rest,routes-with-rest-dsl-get,routes-with-rest-dsl-post,routes-with-rest-get,routes-with-rest-post")); - RestAssured.given() - .get("/groovy-dsl/main/successful/routes") - .then() - .statusCode(200) - .body(CoreMatchers.is("5")); + void testMainInstanceWithJavaRoutes() throws Exception { + try (CloseableHttpClient client = HttpClientBuilder.create().build()) { + // Given + HttpUriRequest request = new HttpGet(toAbsolutePath("/groovy-dsl/main/groovyRoutesBuilderLoader")); + + // When + HttpResponse httpResponse = client.execute(request); + + // Then + assertThat(httpResponse.getStatusLine().getStatusCode()).isEqualTo(HttpStatus.SC_OK); + assertThat(EntityUtils.toString(httpResponse.getEntity())).isEqualTo(GroovyRoutesBuilderLoader.class.getName()); + + // Given + request = new HttpGet(toAbsolutePath("/groovy-dsl/main/routeBuilders")); + + // When + httpResponse = client.execute(request); + + // Then + assertThat(httpResponse.getStatusLine().getStatusCode()).isEqualTo(HttpStatus.SC_OK); + assertThat(EntityUtils.toString(httpResponse.getEntity())).isEmpty(); + + // Given + request = new HttpGet(toAbsolutePath("/groovy-dsl/main/routes")); + + // When + httpResponse = client.execute(request); + + // Then + assertThat(httpResponse.getStatusLine().getStatusCode()).isEqualTo(HttpStatus.SC_OK); + assertThat(EntityUtils.toString(httpResponse.getEntity())).isEqualTo( + "my-groovy-route,routes-with-components-configuration,routes-with-dataformats-configuration,routes-with-eip-body,routes-with-eip-exchange,routes-with-eip-message,routes-with-eip-process,routes-with-eip-setBody,routes-with-endpoint-dsl,routes-with-error-handler,routes-with-languages-configuration,routes-with-rest,routes-with-rest-dsl-get,routes-with-rest-dsl-post,routes-with-rest-get,routes-with-rest-post"); + + // Given + request = new HttpGet(toAbsolutePath("/groovy-dsl/main/successful/routes")); + + // When + httpResponse = client.execute(request); + + // Then + assertThat(httpResponse.getStatusLine().getStatusCode()).isEqualTo(HttpStatus.SC_OK); + assertThat(EntityUtils.toString(httpResponse.getEntity())).isEqualTo("10"); + } } @Test - void testRestEndpoints() { - RestAssured.given() - .get("/root/my/path/get") - .then() - .statusCode(200) - .body(CoreMatchers.is("Hello World")); - RestAssured.given() - .body("Will") - .post("/root/post") - .then() - .statusCode(200) - .body(CoreMatchers.is("Hello Will")); + void testRestEndpoints() throws Exception { + try (CloseableHttpClient client = HttpClientBuilder.create().build()) { + // Given + final HttpGet httpGet = new HttpGet(toAbsolutePath("/root/my/path/get")); + + // When + HttpResponse httpResponse = client.execute(httpGet); + + // Then + assertThat(httpResponse.getStatusLine().getStatusCode()).isEqualTo(HttpStatus.SC_OK); + assertThat(EntityUtils.toString(httpResponse.getEntity())).isEqualTo("Hello World"); + + // Given + HttpPost httpPost = new HttpPost(toAbsolutePath("/root/post")); + httpPost.setEntity(new StringEntity("Will", ContentType.TEXT_PLAIN)); + + // When + httpResponse = client.execute(httpPost); + + // Then + assertThat(httpResponse.getStatusLine().getStatusCode()).isEqualTo(HttpStatus.SC_OK); + assertThat(EntityUtils.toString(httpResponse.getEntity())).isEqualTo("Hello Will"); + } } }